Repository navigation
596 lines (567 loc) · 26.3 KB
/
Copy pathbuild.yml
File metadata and controls
596 lines (567 loc) · 26.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
name: build
on:
workflow_call:
inputs:
# Profile is the canonical input. If set, all other fields are resolved from
# config/profiles/<profile>.yaml in the CALLER's repo (not winforge).
profile:
required: false
type: string
default: ""
# Direct override inputs (for one-off builds that don't warrant a profile)
product:
required: false
type: string
default: ""
edition:
required: false
type: string
default: ""
language:
required: false
type: string
default: ""
compression:
required: false
type: string
default: ""
label:
required: false
type: string
default: ""
uup_uuid:
required: false
type: string
default: ""
uup_title:
required: false
type: string
default: ""
autounattend_xml:
required: false
type: string
default: ""
# Path inside the caller's repo where config lives. Defaults to ./ (root).
# Useful for monorepos that keep winforge configs in a subdirectory.
config_root:
required: false
type: string
default: ""
# Override the winforge version to pull. Default: pinned major.minor.
# Use a SHA for bleeding-edge, or @main for dev. Production should pin.
winforge_ref:
required: false
type: string
default: "v1"
secrets:
RCLONE_CONF:
required: false
ACCOUNTS_YAML:
required: false
GOFILE_TOKEN:
required: false
# Allow direct triggers on the winforge repo itself (legacy / self-build mode).
# When a user calls `uses: win-forge/winforge/.github/workflows/build.yml@v1`,
# only workflow_call fires. These other triggers let winforge build itself
# without needing a separate config repo.
repository_dispatch:
types: [build-request]
workflow_dispatch:
inputs:
profile:
description: "Profile name from config/profiles/ (winforge self-build only)"
required: false
type: string
default: "win11-prod"
# One ISO build per label at a time — concurrent builds would race for the
# same artifacts/ dir and double-spend the ~100GB LVM volume.
concurrency:
group: winforge-build-${{ inputs.profile || github.event.client_payload.profile || github.ref }}
cancel-in-progress: false
jobs:
build:
# Ubuntu: UUP-dump converter uses wimlib, cabextract, chntpw, genisoimage
# (Linux-only). Windows runner doesn't have these without manual install.
runs-on: ubuntu-latest
timeout-minutes: 90
env:
# Resolve the profile (if given) into concrete env vars. Direct inputs
# override profile values, so a dispatch with `profile: win11-prod, edition:
# enterprise` works (and overrides only edition).
PROFILE: ${{ inputs.profile || github.event.client_payload.profile }}
PRODUCT: ${{ inputs.product || github.event.client_payload.product }}
EDITION: ${{ inputs.edition || github.event.client_payload.edition }}
LANGUAGE: ${{ inputs.language || github.event.client_payload.language }}
COMPRESSION: ${{ inputs.compression || github.event.client_payload.compression }}
LABEL: ${{ inputs.label || inputs.profile || github.event.client_payload.label }}
UUP_UUID: ${{ inputs.uup_uuid || github.event.client_payload.uup_uuid }}
UUP_TITLE: ${{ inputs.uup_title || github.event.client_payload.uup_title }}
AUTOU_XML: ${{ inputs.autounattend_xml || github.event.client_payload.autounattend_xml || '' }}
# Where the caller's config lives. Empty string = repo root.
CONFIG_ROOT: ${{ inputs.config_root }}
# Winforge ref. v1 = stable major-version tag (recommended).
WINFORGE_REF: ${{ inputs.winforge_ref }}
# Hoist secrets to job-level env so step `if:` conditions can gate on
# their presence. Direct `secrets.X` access in step `if:` is rejected
# by the workflow parser in some trigger contexts (workflow_dispatch
# on the same repo). env.X is the supported pattern.
HAS_ACCOUNTS_YAML: ${{ secrets.ACCOUNTS_YAML != '' }}
HAS_RCLONE_CONF: ${{ secrets.RCLONE_CONF != '' }}
steps:
# Free disk space BEFORE checkout (this action remounts $GITHUB_WORKSPACE).
# UUP→WIM conversion can hit 8GB+ intermediate; runner ships 25-29GB free
# of 84GB total. Concat / and /mnt via LVM to get ~60GB usable.
- name: Maximize build disk space
# Pinned to v10 (latest release tag) instead of @master — a floating
# third-party ref is a supply-chain risk (the action runs with broad
# runner permissions and remounts the workspace).
uses: easimon/maximize-build-space@v10
with:
# Leave room for apt installs (~500MB) + a buffer. Conversion peak
# is ~8GB WIM + ~5GB UUPs.
root-reserve-mb: 8192
temp-reserve-mb: 2048
swap-size-mb: 2048
# Strip preinstalled runtimes we don't need. None of these are
# used by wimlib/cabextract/genisoimage.
remove-android: 'true'
remove-dotnet: 'true'
remove-haskell: 'true'
remove-codeql: 'true'
# The build doesn't use Docker (UUP-dump converter is a shell script,
# not a container). Pulled images on a fresh runner are minimal
# (~0GB) but explicit-true keeps the action deterministic.
remove-docker-images: 'true'
# The action above frees / and /mnt, but the runner's *own* log volume
# (where _diag/Worker logs are written) is a separate filesystem that
# can fill up on long-running builds and cause the runner to crash
# mid-step with ENOSPC. Clear old logs + caches from the runner volume
# too. Must run AFTER the maximize-build-space step (which may need
# /mnt for loopback images) but BEFORE the build (which produces logs).
- name: Clear runner diagnostic logs + cache
shell: bash
run: |
echo "Runner volume before cleanup:"
df -h /home/runner 2>/dev/null || df -h / || true
# Truncate _diag logs to 0 bytes (preserves file handles the runner
# has open, but releases the disk blocks)
sudo find /home/runner -path '*/_diag/Worker_*.log' -size +10M \
-exec sudo truncate -s 0 {} \; 2>/dev/null || true
# Remove old run cache dirs (anything older than the current run id)
# The runner is _diag/<version>/_diag/Worker_<runid>.log; we keep
# only the most recent one. Older versions' diag logs are dead weight.
sudo find /home/runner -path '*/actions-runner/cached/*/_diag/Worker_*.log' \
-mmin +5 -delete 2>/dev/null || true
# Optional: remove pre-installed toolchains we don't need (Python
# is reinstalled by setup-python@v6 below, so this is safe to clear)
sudo rm -rf /opt/hostedtoolcache/CodeQL 2>/dev/null || true
echo "Runner volume after cleanup:"
df -h /home/runner 2>/dev/null || df -h / || true
# Checkout logic for the three call modes:
#
# 1. workflow_call (called from a config repo):
# - Default checkout checks out the CALLER (config repo) at the root
# - We need winforge's scripts; checkout winforge into .winforge/
# 2. repository_dispatch / workflow_dispatch on win-forge/winforge:
# - Default checkout checks out winforge itself at the root
# - scripts/ + config/ are already at repo root
# 3. repository_dispatch / workflow_dispatch on a non-winforge repo
# (e.g. winforge-configs dispatches its own workflow):
# - Default checkout checks out that repo at the root
# - We need winforge's scripts; checkout winforge into .winforge/
#
# Detection: if the default checkout is winforge itself (it has
# scripts/), we don't need a second checkout. Otherwise we do.
- uses: actions/checkout@v6
- name: Detect if default checkout is winforge
id: detect
shell: bash
run: |
if [ -d scripts ] && [ -d .github ]; then
echo "is_winforge=true" >> "$GITHUB_OUTPUT"
echo "Detected: default checkout is winforge itself (self-build mode)"
else
echo "is_winforge=false" >> "$GITHUB_OUTPUT"
echo "Detected: default checkout is a config repo (caller mode)"
fi
- name: Symlink .winforge -> . in self-build mode
if: steps.detect.outputs.is_winforge == 'true'
shell: bash
run: |
# In self-build mode, the default checkout IS winforge.
# Make .winforge a symlink so all subsequent steps can use the
# same path regardless of mode.
ln -s . .winforge
ls -la .winforge/ | head -5
- name: Checkout winforge into .winforge/ (caller mode)
if: steps.detect.outputs.is_winforge == 'false'
uses: actions/checkout@v6
with:
repository: win-forge/winforge
ref: ${{ env.WINFORGE_REF }}
path: .winforge
- uses: actions/setup-python@v6
with:
python-version: "3.11"
cache: pip
cache-dependency-path: .winforge/pyproject.toml
- name: Install Python deps
# In both modes, install from ./.winforge — it's a symlink to .
# in self-build mode, or a real checkout of winforge in caller mode.
run: pip install -e ./.winforge
env:
# Ensure scripts/ is importable even before pip install finishes
PYTHONPATH: ${{ github.workspace }}/.winforge/scripts
- name: Resolve profile into build env vars
if: env.PROFILE != ''
id: profile
env:
# The profile loader looks in $CONFIG_ROOT/config/profiles/ first,
# then falls back to ./config (self-build on winforge).
WINFORGE_CONFIG_ROOT: ${{ env.CONFIG_ROOT }}
run: |
# Pick the right config root: caller-supplied, or the default
# checkout (which is the config repo or winforge itself).
if [ -n "$WINFORGE_CONFIG_ROOT" ]; then
CFG="$WINFORGE_CONFIG_ROOT"
elif [ -d config ]; then
CFG="."
else
CFG=".winforge"
fi
echo "Loading profile from: $CFG/config/profiles/$PROFILE.yaml"
# Get unquoted KEY=val lines for GITHUB_ENV (env vars don't need quoting)
# and quoted KEY="val" for shell eval (CLI consumers).
eval_lines=$(cd "$CFG" && python -m scripts.profiles.load "$PROFILE")
# Strip the quoting that the script emits for shell eval safety.
# Lines look like: KEY="value" — we want KEY=value for GITHUB_ENV.
unquoted_lines=$(echo "$eval_lines" | sed 's/="\([^"]*\)"$/=\1/')
echo "$unquoted_lines" >> "$GITHUB_ENV"
echo "Resolved profile $PROFILE:"
for k in PRODUCT EDITION LANGUAGE COMPRESSION LABEL UUP_UUID UUP_TITLE; do
echo " $k=${!k}"
done
- name: Install system deps for UUP conversion
run: |
sudo apt-get update
sudo apt-get install -y aria2 cabextract wimtools chntpw genisoimage xorriso isolinux uuid-runtime rclone
# xorriso needs isohdpfx.bin from isolinux for hybrid MBR
ls -la /usr/lib/ISOLINUX/isohdpfx.bin || sudo apt-get install -y syslinux-common
# Render {{PLACEHOLDER}} tokens in the autounattend template with values
# from GitHub Actions Secrets. Writes the rendered XML to AUTOU_XML env
# var so the existing "Write rendered autounattend to disk" step picks
# it up unchanged. Runs unconditionally — every build should embed
# credentials when they're configured.
#
# Template lookup order: <config_root>/autounattend/<product>.xml, then
# <config_root>/autounattend/base.xml, then .winforge/autounattend/base.xml
# (winforge's default no-credential fallback for self-build).
- name: Render autounattend from template + secrets
id: render_autou
shell: bash
env:
PRODUCT_KEY: ${{ secrets.PRODUCT_KEY }}
LOCAL_ADMIN_NAME: ${{ secrets.LOCAL_ADMIN_NAME }}
LOCAL_ADMIN_PASS: ${{ secrets.LOCAL_ADMIN_PASS }}
COMPUTER_NAME: ${{ secrets.COMPUTER_NAME }}
run: |
mkdir -p artifacts/autounattend
# Pick the most-specific template available, then fall back.
# Default checkout (caller's repo) first, then winforge's vendored
# defaults. Order: <config_root>/<product>.xml, ./<product>.xml,
# .winforge/<product>.xml, ./base.xml, .winforge/base.xml.
CFG=""
if [ -n "$CONFIG_ROOT" ]; then CFG="$CONFIG_ROOT"; fi
TEMPLATE=""
for root in "$CFG" .winforge .; do
if [ -n "$root" ] && [ -d "$root" ]; then
if [ -n "${PRODUCT:-}" ] && [ -f "$root/autounattend/${PRODUCT}.xml" ]; then
TEMPLATE="$root/autounattend/${PRODUCT}.xml"
break
elif [ -f "$root/autounattend/base.xml" ]; then
TEMPLATE="$root/autounattend/base.xml"
break
fi
fi
done
if [ -z "$TEMPLATE" ]; then
echo "No autounattend template found in $CFG, ., or .winforge; skipping render"
exit 0
fi
echo "Using autounattend template: $TEMPLATE"
# If the template has no {{...}} placeholders, copy verbatim
if ! grep -q '{{' "$TEMPLATE"; then
echo "Template $TEMPLATE has no placeholders; copying verbatim"
cp "$TEMPLATE" artifacts/autounattend/win11.xml
# Multi-line env var — use heredoc with delimiter (see below)
{
echo "AUTOU_XML<<XML_EOF"
cat artifacts/autounattend/win11.xml
echo ""
echo "XML_EOF"
} >> "$GITHUB_ENV"
exit 0
fi
# Render with values from env (each secret already a string)
RENDERED=$(PRODUCT_KEY="$PRODUCT_KEY" \
LOCAL_ADMIN_NAME="$LOCAL_ADMIN_NAME" \
LOCAL_ADMIN_PASS="$LOCAL_ADMIN_PASS" \
COMPUTER_NAME="$COMPUTER_NAME" \
python -c "
import os, sys
from pathlib import Path
from scripts.build.inject_autounattend import render
keys = ['PRODUCT_KEY', 'LOCAL_ADMIN_NAME', 'LOCAL_ADMIN_PASS', 'COMPUTER_NAME']
values = {k: os.environ.get(k, '') for k in keys}
src = Path('$TEMPLATE').read_text()
sys.stdout.write(render(src, values))
")
# Validate: no placeholders left behind (means a secret was missing)
if echo "$RENDERED" | grep -q '{{'; then
echo "::error::Rendered autounattend still contains {{...}} placeholders. A secret is missing?"
echo "$RENDERED" | grep -oE '\{\{[A-Z0-9_]+\}\}' | sort -u
exit 1
fi
printf '%s' "$RENDERED" > artifacts/autounattend/win11.xml
# Multi-line content in $GITHUB_ENV trips Actions' env-file parser
# (it expects KEY=value, not block scalars). Use heredoc with a
# delimiter, which is the documented way to embed multi-line values.
{
echo "AUTOU_XML<<XML_EOF"
printf '%s' "$RENDERED"
echo ""
echo "XML_EOF"
} >> "$GITHUB_ENV"
echo "Rendered $TEMPLATE -> artifacts/autounattend/win11.xml ($(wc -c < artifacts/autounattend/win11.xml) bytes)"
- name: Sync Intel RST drivers
# .winforge is a symlink to . in self-build mode, or a real checkout
# of winforge in caller mode. Either way, scripts/ is at .winforge/scripts/.
shell: bash
run: python .winforge/scripts/drivers/sync_intel_rst.py
- name: Download UUP files + convert to ISO
shell: bash
env:
COMPRESSION: ${{ env.COMPRESSION }}
run: |
mkdir -p artifacts
# Pass compression through to convert.sh.
bash .winforge/scripts/build/convert.sh "$UUP_UUID" "$EDITION" artifacts/ "$COMPRESSION"
- name: Inject Intel RST drivers into WIM
shell: bash
run: |
WIM="artifacts/install.wim"
if [ ! -f "$WIM" ]; then
echo "No install.wim found — extracting from ISO"
7z x artifacts/iso-in.iso -oartifacts/iso-extract -bd -y >/dev/null
# ESD compression produces install.esd, not install.wim.
# wimlib-imagex handles both formats; normalize the name.
if [ -f artifacts/iso-extract/sources/install.esd ] && [ ! -f artifacts/iso-extract/sources/install.wim ]; then
cp artifacts/iso-extract/sources/install.esd "$WIM"
echo "Note: using install.esd (ESD compression) renamed to install.wim for injection"
else
cp artifacts/iso-extract/sources/install.wim "$WIM"
fi
fi
# wimlib can inject drivers on Linux without DISM
# Drivers come from .winforge/drivers/pack/intel-rst (synced above).
RST_ROOT=".winforge/drivers/pack/intel-rst"
if [ -d "$RST_ROOT" ]; then
DRIVERS=$(find "$RST_ROOT" -name "iaStorAC.inf" -printf '%h\n' | head -1)
if [ -n "$DRIVERS" ]; then
echo "Injecting driver from: $DRIVERS"
wimlib-imagex update "$WIM" 1 --command="add-driver $DRIVERS" 2>&1 | tail -3
else
echo "No iaStorAC.inf found in $RST_ROOT — skipping driver injection"
fi
else
echo "No Intel RST drivers synced — skipping injection"
fi
# Win11 bypass policy: check whether this product+edition needs the
# DLL patch (some 24H2 Enterprise / 25H2 LTSC SKUs block the registry
# trick). The policy reads the `needs_dll_bypass` flag from
# editions.yaml. If needed + DLLs present → stage + patch. If needed
# + DLLs missing → fail the build with a clear error. If not needed →
# skip the DLL steps entirely (saves WIM mount/unmount time).
- name: Check Win11 bypass policy
id: bypass_policy
shell: bash
run: |
RESULT=$(python .winforge/scripts/build/bypass_policy.py \
--product "$PRODUCT" \
--edition "$EDITION" \
--bypass-root .winforge/bypass)
echo "$RESULT"
# Parse action=skip|patch|fail
ACTION=$(echo "$RESULT" | grep '^action=' | cut -d= -f2)
echo "action=$ACTION" >> "$GITHUB_OUTPUT"
- name: Stage Win11 bypass DLLs (vendored)
if: steps.bypass_policy.outputs.action == 'patch'
shell: bash
run: |
BYPASS_SRC=".winforge/bypass/${PRODUCT}"
mkdir -p artifacts/bypass
cp "$BYPASS_SRC"/appraiserres.dll "$BYPASS_SRC"/appraiser.dll artifacts/bypass/
test -f artifacts/bypass/appraiserres.dll \
|| { echo "::error::$BYPASS_SRC missing appraiserres.dll"; exit 1; }
test -f artifacts/bypass/appraiser.dll \
|| { echo "::error::$BYPASS_SRC missing appraiser.dll"; exit 1; }
ls -la artifacts/bypass/
echo "DLL patch enabled (vendored from $BYPASS_SRC)"
- name: Apply Win11 system-requirement bypass
shell: bash
run: |
BYPASS_ARGS=(--wim artifacts/install.wim --mount artifacts/mount)
if [ -d artifacts/bypass ]; then
BYPASS_ARGS+=(--bypass-dir artifacts/bypass)
echo "DLL patch + registry tweak"
else
echo "Registry tweak only (autounattend LabConfig keys)"
fi
python .winforge/scripts/build/bypass_win11_requirements.py "${BYPASS_ARGS[@]}"
- name: Write rendered autounattend to disk
if: env.AUTOU_XML != ''
shell: bash
run: |
mkdir -p artifacts/autounattend
printf '%s' "$AUTOU_XML" > artifacts/autounattend/win11.xml
- name: Repack ISO with autounattend
shell: bash
run: |
AUTOU="artifacts/autounattend/win11.xml"
[ -f "$AUTOU" ] || AUTOU=".winforge/autounattend/base.xml"
# Use the LABEL from the profile for the final ISO filename
OUT_NAME="${LABEL:-${PRODUCT}-${EDITION}}"
bash .winforge/scripts/build/repack.sh \
artifacts/iso-in.iso \
"artifacts/${OUT_NAME}.iso" \
artifacts/install.wim \
"$AUTOU"
# Regression guard: parse the El Torito boot catalog and fail the build
# if the ISO isn't UEFI-bootable. Catches the --grub2-boot-info bug
# class (and any future flag that silently drops the EFI section).
# 'file win11.iso' only reports the MBR/BIOS indicator and silently
# passes when the EFI entry is empty; only a catalog parse catches it.
#
# IMPORTANT: only the repacked output ISO (artifacts/<OUT_NAME>.iso)
# is verified — not artifacts/iso-in.iso. The source ISO from UUP-dump
# conversion has an EFI section header with platform=0x00 (not 0xEF),
# which is a known intermediate artifact of the UUP converter. The
# repack step fixes this; only the post-repack output is expected to
# be UEFI-bootable.
- name: Verify ISO is UEFI bootable
shell: bash
run: |
OUT_NAME="${LABEL:-${PRODUCT}-${EDITION}}"
ISO="artifacts/${OUT_NAME}.iso"
if [ ! -f "$ISO" ]; then
echo "::error::Expected ISO not found at $ISO (repack step didn't produce it?)"
exit 1
fi
echo "--- $ISO ---"
# --json output goes to the log for human-readable debugging.
# PIPESTATUS[0] captures python's exit code (the |tee would
# otherwise mask it because GitHub's default bash doesn't set
# pipefail).
python -m scripts.build.verify_iso_bootable "$ISO" --json \
| tee artifacts/boot-check.json
rc=${PIPESTATUS[0]}
if [ "$rc" -ne 0 ]; then
echo "::error::ISO boot verification failed (rc=$rc). The ISO is not UEFI-bootable."
echo "::error::See artifacts/boot-check.json and the warnings above."
fi
exit "$rc"
# gofile.io is the default upload target — it always runs (guest mode
# if GOFILE_TOKEN is empty, ~10-day expiry). The Google Drive / rclone
# path is gated on ACCOUNTS_YAML being configured: skip the assign +
# upload steps entirely when no accounts are available so the build
# doesn't fail downstream on "No account can host <product>".
- name: Assign upload account
id: assign
if: env.HAS_ACCOUNTS_YAML == 'true'
shell: bash
env:
ACCOUNTS_YAML: ${{ secrets.ACCOUNTS_YAML }}
run: |
echo "$ACCOUNTS_YAML" > /tmp/accounts.yaml
ACCOUNT=$(cd .winforge && python -m scripts.rclone.assign "${PRODUCT}" "6.0" /tmp/accounts.yaml)
echo "account=$ACCOUNT" >> $GITHUB_OUTPUT
- name: Upload ISO to Google Drive
if: env.HAS_ACCOUNTS_YAML == 'true' && env.HAS_RCLONE_CONF == 'true'
shell: bash
env:
RCLONE_CONF: ${{ secrets.RCLONE_CONF }}
run: |
mkdir -p ~/.config/rclone
printf '%s' "$RCLONE_CONF" > ~/.config/rclone/rclone.conf
OUT_NAME="${LABEL:-${PRODUCT}-${EDITION}}"
bash .winforge/scripts/rclone/upload.sh \
"artifacts/${OUT_NAME}.iso" \
"${{ steps.assign.outputs.account }}" \
"iso/${PRODUCT}/" \
~/.config/rclone/rclone.conf
- name: Upload ISO to gofile.io
id: gofile
shell: bash
env:
GOFILE_TOKEN: ${{ secrets.GOFILE_TOKEN }}
run: |
if [ -z "${GOFILE_TOKEN}" ]; then
echo "::warning::GOFILE_TOKEN secret is empty — uploading as guest (will expire)"
fi
OUT_NAME="${LABEL:-${PRODUCT}-${EDITION}}"
URLS=$(python -m scripts.gofile.upload \
"artifacts/${OUT_NAME}.iso" \
--product "$PRODUCT" \
--edition "$EDITION")
echo "$URLS" | tee artifacts/gofile-urls.txt
# Publish a checksum so downloaders can verify the ISO end-to-end.
ISO_SHA=$(sha256sum "artifacts/${OUT_NAME}.iso" | cut -d' ' -f1)
echo "$ISO_SHA ${OUT_NAME}.iso" > artifacts/SHA256SUMS
echo "ISO sha256: $ISO_SHA"
# Parse key=value lines from the script's stdout. Tolerate empty values
# and the guest-token warning line (no '='). Skips nothing — extra lines
# just get ignored.
declare -A GOFILE
while IFS='=' read -r k v; do
[ -n "$k" ] && GOFILE["$k"]="$v"
done <<< "$URLS"
# Step summary (visible in the Actions run UI on GitHub)
{
echo "## gofile.io upload (default upload target)"
echo ""
echo "- **Product / edition:** \`$PRODUCT / $EDITION\`"
echo "- **ISO:** \`${OUT_NAME}.iso\`"
if [ -n "${ISO_SHA:-}" ]; then
echo "- **SHA256:** \`$ISO_SHA\`"
fi
if [ -n "${GOFILE[folder_url]:-}" ]; then
echo "- **Download page:** ${GOFILE[folder_url]}"
fi
if [ -n "${GOFILE[direct_url]:-}" ]; then
echo "- **Direct link:** ${GOFILE[direct_url]}"
fi
if [ -z "${GOFILE_TOKEN}" ]; then
echo "- **Mode:** guest (content expires in ~10 days)"
else
echo "- **Mode:** account (persistent)"
fi
} >> "$GITHUB_STEP_SUMMARY"
# Outputs for caller workflows (Discord webhook, etc — just read these)
echo "folder_url=${GOFILE[folder_url]:-}" >> "$GITHUB_OUTPUT"
echo "file_id=${GOFILE[file_id]:-}" >> "$GITHUB_OUTPUT"
echo "folder_id=${GOFILE[folder_id]:-}" >> "$GITHUB_OUTPUT"
echo "direct_url=${GOFILE[direct_url]:-}" >> "$GITHUB_OUTPUT"
echo "manager_url=${GOFILE[manager_url]:-}" >> "$GITHUB_OUTPUT"
if [ -n "${GOFILE[guest_token]:-}" ]; then
echo "guest_token=${GOFILE[guest_token]}" >> "$GITHUB_OUTPUT"
fi
- name: Upload ISO artifact (debug)
if: always()
uses: actions/upload-artifact@v7
with:
name: iso-${{ env.LABEL || format('{0}-{1}', env.PRODUCT, env.EDITION) }}
path: |
artifacts/*.iso
artifacts/SHA256SUMS
retention-days: 7
if-no-files-found: ignore