From 7f171cf52b5353183e63e7deb3303a4b099a960e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 03:00:11 +0000 Subject: [PATCH] Add "Build kiosk ISO" GitHub Actions workflow Manually triggered (workflow_dispatch): pick interactive / unattended-kiosk / fully-automatic, optionally pre-set the admin user (password from the KIOSK_ADMIN_PASSWORD repo secret, never a logged input), hostname and SSH key. Runs iso/build-iso.sh and uploads the ISO plus its sha256 as a run artifact (uncompressed, 3-day default retention). build-iso.sh changes to run in CI: - Password can come from $KIOSK_ADMIN_PASSWORD; with no TTY and no password it fails clearly instead of hanging on a prompt. - Stage the repo by fetching the commit rather than `git clone --branch`, which fails on actions/checkout's detached HEAD; the branch name falls back to $GITHUB_REF_NAME, then main, and is set to track origin so Advanced -> Upgrade works on the installed kiosk. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm --- .github/workflows/build-iso.yml | 121 ++++++++++++++++++++++++++++++++ Readme.md | 8 +++ iso/build-iso.sh | 30 ++++++-- 3 files changed, 152 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/build-iso.yml diff --git a/.github/workflows/build-iso.yml b/.github/workflows/build-iso.yml new file mode 100644 index 0000000..5878caa --- /dev/null +++ b/.github/workflows/build-iso.yml @@ -0,0 +1,121 @@ +# Builds the all-in-one Ubuntu Server 26.04 + kiosk install ISO +# (iso/build-iso.sh) on demand: Actions tab -> "Build kiosk ISO" -> +# Run workflow. The ISO (~2.8 GB) is attached to the run as a downloadable +# artifact for a few days - see the run's Summary page. +# +# Pre-setting the admin account (needed for "fully-automatic") uses the +# repository secret KIOSK_ADMIN_PASSWORD (Settings -> Secrets and +# variables -> Actions) - never a workflow input, since inputs are shown +# in the run log. Only a SHA-512 hash of it goes on the ISO, but anyone +# who downloads the ISO can read that hash, so treat the artifact as +# private and use a password you'll change after install. +name: Build kiosk ISO + +on: + workflow_dispatch: + inputs: + mode: + description: >- + interactive: installer asks network/disk/user, kiosk install.sh + runs with prompts on first boot. unattended-kiosk: same installer, + kiosk installs with defaults and reboots into the kiosk. + fully-automatic: no questions at all - WIPES THE LARGEST DISK + (needs username + KIOSK_ADMIN_PASSWORD secret). + type: choice + default: interactive + options: + - interactive + - unattended-kiosk + - fully-automatic + username: + description: Admin username to pre-set (blank = asked during install). Needs the KIOSK_ADMIN_PASSWORD secret. Not "kiosk"/"admin". + type: string + default: "" + hostname: + description: Hostname (only used when username is set) + type: string + default: kiosk + ssh_public_key: + description: Optional SSH public key for the admin user (ssh-ed25519 AAAA...) + type: string + default: "" + retention_days: + description: Days to keep the ISO artifact + type: number + default: 3 + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-24.04 + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + + - name: Install build tools + run: | + sudo apt-get update -q + sudo apt-get install -y -q xorriso + + - name: Build ISO + env: + MODE: ${{ inputs.mode }} + ADMIN_USERNAME: ${{ inputs.username }} + ADMIN_HOSTNAME: ${{ inputs.hostname }} + SSH_PUBLIC_KEY: ${{ inputs.ssh_public_key }} + KIOSK_ADMIN_PASSWORD: ${{ secrets.KIOSK_ADMIN_PASSWORD }} + run: | + mkdir -p "$RUNNER_TEMP/iso" + args=(--output "$RUNNER_TEMP/iso/ubuntu-kiosk-amd64.iso") + case "$MODE" in + unattended-kiosk) args+=(--unattended) ;; + fully-automatic) args+=(--fully-automatic) ;; + esac + if [[ -n "$ADMIN_USERNAME" ]]; then + if [[ -z "$KIOSK_ADMIN_PASSWORD" ]]; then + echo "::error::username given but the KIOSK_ADMIN_PASSWORD repository secret isn't set" + exit 1 + fi + args+=(--username "$ADMIN_USERNAME" --hostname "$ADMIN_HOSTNAME") + fi + if [[ -n "$SSH_PUBLIC_KEY" ]]; then + printf '%s\n' "$SSH_PUBLIC_KEY" > "$RUNNER_TEMP/ssh_key.pub" + args+=(--ssh-key "$RUNNER_TEMP/ssh_key.pub") + fi + iso/build-iso.sh "${args[@]}" + # Name the file after the Ubuntu point release the script picked. + src="$(find iso/build/cache -name "*.iso" -print -quit)" + name="$(basename "$src" .iso | sed 's/-live-server/-kiosk/')-${MODE}-${GITHUB_SHA::7}.iso" + mv "$RUNNER_TEMP/iso/ubuntu-kiosk-amd64.iso" "$RUNNER_TEMP/iso/$name" + # Free the ~3 GB source ISO before the upload step needs scratch space. + rm -rf iso/build/cache + (cd "$RUNNER_TEMP/iso" && sha256sum "$name" > "$name.sha256") + echo "ISO_NAME=$name" >> "$GITHUB_ENV" + { + echo "### $name" + echo + echo "- Mode: \`$MODE\`" + echo "- Admin user: ${ADMIN_USERNAME:-chosen during install}" + echo "- Built from: \`${GITHUB_REF_NAME}\` @ \`${GITHUB_SHA::7}\`" + echo "- SHA-256: \`$(cut -d' ' -f1 "$RUNNER_TEMP/iso/$name.sha256")\`" + echo + echo "Download it from **Artifacts** below (GitHub zips it; unzip, then write the .iso to a USB stick)." + if [[ "$MODE" == "fully-automatic" ]]; then + echo + echo "> **Warning:** this ISO wipes the target machine's largest disk without asking." + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload ISO + uses: actions/upload-artifact@v4 + with: + name: ${{ env.ISO_NAME }} + path: | + ${{ runner.temp }}/iso/${{ env.ISO_NAME }} + ${{ runner.temp }}/iso/${{ env.ISO_NAME }}.sha256 + # An ISO is already compressed - zipping it again only costs time. + compression-level: 0 + retention-days: ${{ inputs.retention_days }} + if-no-files-found: error diff --git a/Readme.md b/Readme.md index cf5e1e6..dd0c94e 100644 --- a/Readme.md +++ b/Readme.md @@ -96,6 +96,14 @@ Useful options (`iso/build-iso.sh --help` for all): | `--fully-automatic` | All of the above (needs `--username`): boot it and walk away. **Wipes a disk with no questions — label the USB stick.** | | `--iso PATH` | Use an ISO you already have instead of downloading. | +**No Linux machine handy?** Build it on GitHub instead: **Actions → Build +kiosk ISO → Run workflow**, pick a mode, and download the ISO from the run's +**Artifacts** once it finishes (about 10 minutes; kept for 3 days by default). +To pre-set the admin account (required for `fully-automatic`), first add a +repository secret named `KIOSK_ADMIN_PASSWORD` (Settings → Secrets and +variables → Actions). Passwords are never typed into the workflow form, +because its inputs show up in the run log. + Notes: - The ISO carries a copy of this checkout's current commit (uncommitted changes are not included). diff --git a/iso/build-iso.sh b/iso/build-iso.sh index 4e3f3e2..e591f7b 100755 --- a/iso/build-iso.sh +++ b/iso/build-iso.sh @@ -40,7 +40,10 @@ # ./install.sh as - must not be "kiosk"). Giving this # makes the installer's identity screen automatic. # --password PASS Admin password (prompted for if --username is given -# without it). Stored only as a SHA-512 crypt hash. +# without it, or read from $KIOSK_ADMIN_PASSWORD - +# better than this flag on shared machines/CI, where +# command lines are visible). Stored on the ISO only +# as a SHA-512 crypt hash. # --hostname NAME Hostname (default: kiosk). # --realname NAME Full name for the admin account (default: Kiosk Admin). # --ssh-key FILE Add this public key file to the admin user's @@ -78,7 +81,7 @@ SRC_ISO="" RELEASE="26.04" OUTPUT="" ADMIN_USER="" -ADMIN_PASS="" +ADMIN_PASS="${KIOSK_ADMIN_PASSWORD:-}" HOSTNAME_="kiosk" REALNAME="Kiosk Admin" SSH_KEY_FILE="" @@ -153,6 +156,7 @@ if [[ -n "$ADMIN_USER" ]]; then [[ " $(echo $RESERVED_USERNAMES) " != *" $ADMIN_USER "* ]] \ || die "username '$ADMIN_USER' is reserved by Ubuntu (a system user/group) - pick another" if [[ -z "$ADMIN_PASS" ]]; then + [[ -t 0 ]] || die "--username needs a password: pass --password or set KIOSK_ADMIN_PASSWORD" read -r -s -p "Password for $ADMIN_USER: " ADMIN_PASS; echo read -r -s -p "Confirm password: " pass2; echo [[ "$ADMIN_PASS" == "$pass2" ]] || die "passwords don't match" @@ -216,15 +220,27 @@ fi # Stage the repo + first-boot files that go on the ISO under /kiosk ################################################################################ -info "Staging repo ($(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD) @ $(git -C "$REPO_ROOT" rev-parse --short HEAD))..." +# Branch the copied repo is on - what Advanced -> Upgrade pulls. CI +# checkouts (actions/checkout) are a detached HEAD, so fall back to the +# branch CI says it built, then main. +BRANCH="$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD)" +[[ "$BRANCH" != "HEAD" ]] || BRANCH="${GITHUB_REF_NAME:-main}" +COMMIT="$(git -C "$REPO_ROOT" rev-parse HEAD)" + +info "Staging repo ($BRANCH @ ${COMMIT:0:7})..." if [[ -n "$(git -C "$REPO_ROOT" status --porcelain)" ]]; then echo " WARNING: uncommitted changes in $REPO_ROOT are NOT included - commit them first if you need them." fi mkdir -p "$WORK/kiosk/firstboot" -git clone --quiet --depth 1 --no-local \ - --branch "$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD)" \ - "file://$REPO_ROOT" "$WORK/kiosk/ubuntu-based-kiosk" -git -C "$WORK/kiosk/ubuntu-based-kiosk" remote set-url origin "$REPO_URL" +# init + fetch by commit rather than `git clone --branch`, which can't +# clone a detached HEAD. +stage_repo="$WORK/kiosk/ubuntu-based-kiosk" +git init --quiet "$stage_repo" +git -C "$stage_repo" fetch --quiet --depth 1 "file://$REPO_ROOT" "$COMMIT" +git -C "$stage_repo" checkout --quiet -B "$BRANCH" FETCH_HEAD +git -C "$stage_repo" remote add origin "$REPO_URL" +git -C "$stage_repo" config "branch.$BRANCH.remote" origin +git -C "$stage_repo" config "branch.$BRANCH.merge" "refs/heads/$BRANCH" install -m 755 "$SCRIPT_DIR/firstboot/kiosk-firstboot" "$WORK/kiosk/firstboot/" install -m 644 "$SCRIPT_DIR/firstboot/kiosk-firstboot.service" "$WORK/kiosk/firstboot/"