Skip to content

feat(vps-github-arc): ✨ add GitHub Actions Runner Controller via Helm - #3

Merged
tomgrv merged 8 commits into
developfrom
claude/github-arc-component-pgf8j5
Sep 2, 2026
Merged

feat(vps-github-arc): ✨ add GitHub Actions Runner Controller via Helm#3
tomgrv merged 8 commits into
developfrom
claude/github-arc-component-pgf8j5

Conversation

@tomgrv

@tomgrv tomgrv commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Integrates GitHub Actions Runner Controller (ARC) into this repo's per-feature workspace structure (<name>/{package.json,run.sh}), matching argocd/epinio/etc.
  • Adds github-arc/ (opt-in like argocd/epinio, depends on k3s): installs the gha-runner-scale-set-controller and gha-runner-scale-set Helm charts into the github namespace, per the ARC quickstart
  • Auth is via a GitHub App secret (recommended over a PAT) built from GITHUB_ARC_APP_ID / GITHUB_ARC_APP_INSTALLATION_ID / GITHUB_ARC_APP_PRIVATE_KEY_FILE (a path to the key, never the key content itself, on the command line)
  • Wires it into dispatch.sh (auto-discovered), summary/run.sh (status line), and README.md (menu, down-table, "Getting the keys you'll need", a full section, and the env var table) so it behaves exactly like every other opt-in step
  • Every feature's package.json declares vps.inputs/vps.outputs, in the same shape as a GitHub composite action's inputs:/outputs:, keyed directly by the env var each feature's run.sh reads. dispatch.sh reads this and, on an interactive run, prompts for any input of an enabled step that isn't already set in the environment before running anything
  • dispatch.sh reads every package.json via jq (installed on demand via ensure_jq if missing, since this runs before the system step that would otherwise install it)
  • The three per-step operations dispatch.sh drives (state_get/state_set, ask_missing_inputs, run_step) live in their own sourced file (summary/dispatch-steps.sh)
  • Fixed a bug the hyphenated github-arc name exposed: state_get/state_set eval'd a STATE_<name> variable directly, invalid shell syntax for a name containing a hyphen

Merge note: this branch predated develop's workspace restructuring (features/<name>/<name>/, lib/common.shcommon/run.sh + zz_use, @vps/*@tomgrv/vps-*). @copilot resolved the resulting conflict (9686a47, 6607f13), correctly re-homing github-arc/ and summary/dispatch-steps.sh onto the new layout. I reviewed that resolution: syntax checks pass, github-arc/run.sh now bootstraps zz_use/common like every other feature, and all the shared helpers it calls (require_root, command_exists, helm_teardown, dispatch_action, zz_log) exist in common/run.sh. I can't fully exercise dispatch.sh end-to-end here since setup.sh fetches tomgrv/scripts over the network, which this sandbox blocks - CI is the real signal for that.

Layout

github-arc/
├── package.json   # @tomgrv/vps-github-arc, opt-in, depends on @tomgrv/vps-k3s, vps.inputs/outputs
└── run.sh          # up()/down(): controller + runner scale set, github namespace

Test plan

  • sh -n dispatch.sh, sh -n summary/dispatch-steps.sh, bash -n github-arc/run.sh (syntax checks)
  • Every */package.json parses as valid JSON
  • Verified the interactive prompt loop end-to-end via a pty on the pre-restructure layout (answers a required input, accepts defaults for optional ones, exports them so the TAILSCALE_AUTHKEY guard passes without re-asking)
  • Reviewed Copilot's conflict resolution against the new zz_use/common convention
  • Run sudo sh dispatch.sh --only-github-arc on a real k3s VPS with a configured GitHub App and confirm runners register (network access needed for setup.sh/OCI Helm charts isn't available in this sandbox)

@tomgrv
tomgrv changed the base branch from main to develop August 26, 2026 13:18
Adds features/github-arc (order 9, opt-in, depends on k3s) installing
the gha-runner-scale-set-controller and gha-runner-scale-set Helm
charts into the github namespace, authenticated via a GitHub App
secret, per the ARC quickstart. Wires it into the existing
dispatch.sh/README/lib/summary.sh conventions used by argocd/epinio.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JdfpGmChEUw4bDbDVbbRya
@tomgrv
tomgrv force-pushed the claude/github-arc-component-pgf8j5 branch from 897c5bf to a84371c Compare August 26, 2026 13:25
@tomgrv tomgrv changed the title Add GitHub Actions Runner Controller (ARC) Helm setup Add github-arc feature: GitHub Actions Runner Controller via Helm Aug 26, 2026
@tomgrv tomgrv changed the title Add github-arc feature: GitHub Actions Runner Controller via Helm feat(github-arc): add GitHub Actions Runner Controller via Helm Aug 26, 2026

tomgrv commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

validate-pr-title is showing failure, but that's stale: the workflow only triggers on opened/reopened/ready_for_review/synchronize, and reads the PR title from the triggering event's payload, not live. It last ran against the PR's original title ("Add GitHub Actions Runner Controller (ARC) Helm setup") — a title-only edit doesn't re-trigger it, and re-running the same job replays that same cached payload (confirmed: the re-run's logs still show PR_TITLE: Add GitHub Actions Runner Controller (ARC) Helm setup).

The PR's current title, feat(github-arc): add GitHub Actions Runner Controller via Helm, is conventional-commit-compliant and matches this repo's commitlint config. It will validate clean the next time a real commit is pushed (a synchronize event), so this check goes green with the next content change rather than needing any further title fix.


Generated by Claude Code

claude added 2 commits August 26, 2026 16:32
…ch.sh

Each features/*/package.json now declares vps.inputs/vps.outputs in the
same shape as a GitHub composite action's inputs:/outputs:, keyed
directly by the env var each feature's run.sh reads. dispatch.sh reads
this (new pkg_input_* helpers, brace-depth tracked like pkg_deps) and,
on an interactive run, prompts for any input of an enabled step that
isn't already set in the environment before running anything.

Also fixes a pre-existing bug the hyphenated github-arc feature name
exposed: state_get/state_set eval'd a STATE_<name> variable directly,
which is invalid shell syntax for a name containing a hyphen.
"vps" was a made-up top-level package.json field a schema validator
wouldn't recognize. "config" is npm's own reserved field for
package-specific settings, so every features/*/package.json now nests
order/default/ports/dependencies/inputs/outputs under that instead.

Updates the few places that referenced the key literally: lib/common.sh's
jq lookups (.vps.ports -> .config.ports) and doc comments in dispatch.sh,
lib/common.sh, features/security/run.sh, and README.md. The dispatch.sh
sed/awk parsers themselves needed no change - they match field names
generically regardless of the parent object's name.
@github-actions github-actions Bot changed the title feat(github-arc): add GitHub Actions Runner Controller via Helm feat(github-arc): ✨ add GitHub Actions Runner Controller via Helm Aug 26, 2026
claude added 2 commits August 26, 2026 17:14
state_get/state_set (set), ask_missing_inputs (ask), and run_step (run)
move out of dispatch.sh into their own sourced POSIX sh file, so the root
script stays focused on flag parsing, the menu, and dependency
resolution. Sourced once REPO_ROOT is known, after the bootstrap
clone/re-exec, so the curl | sh path is unaffected.
Replaces the hand-rolled sed/awk pkg_str/pkg_bool/pkg_num/pkg_deps/
pkg_input_names/pkg_input_block parsers with direct jq queries (feature_*
accessors, pkg_input_description/required/default). Adds ensure_jq,
called right before the first package.json read, which installs jq via
apt if it's missing - needed since this runs before features/system (the
step that would otherwise install it) on a totally fresh box.

lib/common.sh already used jq for port lookups; dispatch.sh's own
parsing is now consistent with that instead of being the odd one out.
@tomgrv

tomgrv commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

@copilot resolve the merge conflicts in this pull request

Copilot AI and others added 2 commits September 1, 2026 11:43
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: tomgrv <1809566+tomgrv@users.noreply.github.com>
Co-authored-by: tomgrv <1809566+tomgrv@users.noreply.github.com>

Copilot AI commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved and merged develop's workspace restructuring in 9686a47; the final alignment commit is 6607f13.

Lost in the merge of develop's workspace restructuring - every other
feature's run.sh is 100755.
@tomgrv tomgrv changed the title feat(github-arc): ✨ add GitHub Actions Runner Controller via Helm feat(vps-github-arc): add GitHub Actions Runner Controller via Helm Sep 1, 2026

tomgrv commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

validate-pr-title failed because the scope convention changed along with develop's workspace restructuring: workspace packages are now @tomgrv/vps-<name>, so config-workspace-scopes expects vps-<name> scopes (e.g. vps-github-arc), not the old <name> (e.g. github-arc). Title is now feat(vps-github-arc): add GitHub Actions Runner Controller via Helm.

As before, this workflow only reads the PR title from the triggering event's payload, not live, so the re-run I just queued will likely replay the old (still-failing) title rather than picking up this edit - it'll go green on the next real push (synchronize event).


Generated by Claude Code

@tomgrv

tomgrv commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

@copilot enforce workspace scope

Copilot AI commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

@copilot enforce workspace scope

Workspace scope enforcement is already active through @commitlint/config-workspace-scopes; CI derives and permits vps-github-arc. The current PR title uses that scope, and the prior failure's logs confirm they validated the stale github-arc title payload.

@tomgrv
tomgrv marked this pull request as ready for review September 2, 2026 07:34
@tomgrv
tomgrv merged commit d0793e9 into develop Sep 2, 2026
5 of 8 checks passed
@github-actions github-actions Bot changed the title feat(vps-github-arc): add GitHub Actions Runner Controller via Helm feat(vps-github-arc): ✨ add GitHub Actions Runner Controller via Helm Sep 2, 2026
tomgrv added a commit that referenced this pull request Sep 2, 2026
…ADME.md + test.bats per folder) (#9)

* feat(vps-github-arc): add GitHub Actions Runner Controller via Helm (#3)

* Add github-arc feature: GitHub Actions Runner Controller via Helm

Adds features/github-arc (order 9, opt-in, depends on k3s) installing
the gha-runner-scale-set-controller and gha-runner-scale-set Helm
charts into the github namespace, authenticated via a GitHub App
secret, per the ARC quickstart. Wires it into the existing
dispatch.sh/README/lib/summary.sh conventions used by argocd/epinio.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JdfpGmChEUw4bDbDVbbRya

* Add vps.inputs/outputs to every feature and prompt for them in dispatch.sh

Each features/*/package.json now declares vps.inputs/vps.outputs in the
same shape as a GitHub composite action's inputs:/outputs:, keyed
directly by the env var each feature's run.sh reads. dispatch.sh reads
this (new pkg_input_* helpers, brace-depth tracked like pkg_deps) and,
on an interactive run, prompts for any input of an enabled step that
isn't already set in the environment before running anything.

Also fixes a pre-existing bug the hyphenated github-arc feature name
exposed: state_get/state_set eval'd a STATE_<name> variable directly,
which is invalid shell syntax for a name containing a hyphen.

* Rename package.json's "vps" field to "config" for schema compliance

"vps" was a made-up top-level package.json field a schema validator
wouldn't recognize. "config" is npm's own reserved field for
package-specific settings, so every features/*/package.json now nests
order/default/ports/dependencies/inputs/outputs under that instead.

Updates the few places that referenced the key literally: lib/common.sh's
jq lookups (.vps.ports -> .config.ports) and doc comments in dispatch.sh,
lib/common.sh, features/security/run.sh, and README.md. The dispatch.sh
sed/awk parsers themselves needed no change - they match field names
generically regardless of the parent object's name.

* Split set/ask/run into lib/dispatch-steps.sh

state_get/state_set (set), ask_missing_inputs (ask), and run_step (run)
move out of dispatch.sh into their own sourced POSIX sh file, so the root
script stays focused on flag parsing, the menu, and dependency
resolution. Sourced once REPO_ROOT is known, after the bootstrap
clone/re-exec, so the curl | sh path is unaffected.

* Read package.json with jq in dispatch.sh, installing it on demand

Replaces the hand-rolled sed/awk pkg_str/pkg_bool/pkg_num/pkg_deps/
pkg_input_names/pkg_input_block parsers with direct jq queries (feature_*
accessors, pkg_input_description/required/default). Adds ensure_jq,
called right before the first package.json read, which installs jq via
apt if it's missing - needed since this runs before features/system (the
step that would otherwise install it) on a totally fresh box.

lib/common.sh already used jq for port lookups; dispatch.sh's own
parsing is now consistent with that instead of being the odd one out.

* docs(github-arc): align merge comments with workspace layout

Co-authored-by: tomgrv <1809566+tomgrv@users.noreply.github.com>

* fix(github-arc): restore executable bit on run.sh

Lost in the merge of develop's workspace restructuring - every other
feature's run.sh is 100755.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>

* Align feature folders with tomgrv/scripts convention: README.md + test.bats per folder

Every feature folder now carries its own README.md (usage, up/down,
env vars, dependencies) and test.bats (syntax + shape checks), the same
one-folder-per-step layout tomgrv/scripts uses for its own scripts.
common/'s pure-logic tests moved from tests/test-common.bats into
common/test.bats; tests/test-syntax.bats now covers only what spans the
whole repo (dispatch.sh/setup.sh, cross-folder invariants) instead of
looping over every folder itself. Each package.json gains a "test": "bats
test.bats" script, and the root package.json's own "test" script now runs
the whole suite recursively.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134CCrDUFyYPUmcbZ2NcKKd

* chore: regenerate package-lock.json to drop argocd/epinio/github-arc

Left over as extraneous entries after those workspace folders were
removed in favor of marketplace/ (#7) - npm install
alone didn't prune them, only a fresh lockfile did.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134CCrDUFyYPUmcbZ2NcKKd

* refactor: package.json "name" fields become bare folder names, matching tomgrv/scripts

Every feature's package.json "name" was "@tomgrv/vps-<folder>", diverging
from tomgrv/scripts' own convention where "name" is always exactly the
folder name (no scope, no prefix - see e.g. install-feature/package.json).
"name" and "dependencies" keys are now bare folder names throughout
(rancher's package.json: "name": "rancher", "dependencies": {"k3s": "*",
"common": "*"}).

dispatch.sh's feature_name()/feature_deps() and common/run.sh's
feature_package_json() no longer strip/expect an "@tomgrv/vps-" prefix.
feature_deps() now recognizes a real feature dependency by checking the
key names an actual <folder>/{package.json,run.sh} (excluding
common/summary) instead of by naming-convention prefix, so an ordinary
npm dependency wouldn't be mistaken for an auto-enable target if one is
ever added.

This changes vps-tailscale's dispatch.sh identity from the short
"tailscale" to the full "vps-tailscale" (folder name == package name
now, with no exception) - flags become --only-vps-tailscale/
--skip-vps-tailscale/--down-vps-tailscale, and the TAILSCALE_AUTHKEY
guard's state_get/messages were updated to match. Verified via a real
dispatch.sh --help and --only-vps-tailscale run (not just the test
suite) that the new flag names resolve correctly.

Regenerated package-lock.json for the renamed workspace packages.
README.md and every test.bats/README.md updated to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134CCrDUFyYPUmcbZ2NcKKd

* refactor: rename every feature folder to vps-<name>, matching its scoped package name

Corrects the previous commit's approach: package.json "name" fields stay
scoped as "@tomgrv/vps-<name>" (this repo's own convention, not
tomgrv/scripts' unscoped one - its packages aren't all prefixed the same
way this repo's are), and every folder is renamed to carry the same
"vps-" prefix the package name already had, instead of stripping the
prefix off the package name to match a bare folder. vps-tailscale/ was
already named this way (to avoid zz_use shadowing the real tailscale
CLI) - every other folder now follows the same shape uniformly, so
that's no longer a special case.

Renamed: cockpit -> vps-cockpit, common -> vps-common, dockermanager ->
vps-dockermanager, k3s -> vps-k3s, marketplace -> vps-marketplace,
rancher -> vps-rancher, security -> vps-security, summary -> vps-summary,
system -> vps-system.

- Every package.json's "bin" key and "dependencies" keys now match the
  new folder names / scoped package names.
- dispatch.sh's feature_name()/feature_deps() strip only the npm scope
  ("@tomgrv/") now, not the "vps-" prefix too, so the CLI/state identity
  (--only-<x>/--skip-<x>/--down-<x>, dependency auto-enable) is the full
  "vps-<name>" uniformly - e.g. --only-vps-rancher, not --only-rancher.
  Verified with a real "dispatch.sh --help" and a live
  --skip-vps-tailscale --only-vps-rancher run (dependency auto-enable
  message, step skip messages) rather than just the test suite.
- common/run.sh's feature_package_json() matches package.json "name"
  against "@tomgrv/<feature>" (feature already carries "vps-"), and every
  run.sh's zz_use/". <name>" sourcing of vps-common was updated to match.
- Every test.bats/README.md updated for the new folder/bin/dependency
  names; root package.json's "workspaces" array and package-lock.json
  regenerated for the renamed workspace packages.
- README.md updated throughout (Layout, One folder per feature, flag
  examples, removal table, interactive menu, env var table) to the new
  vps-<name> identity everywhere.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134CCrDUFyYPUmcbZ2NcKKd

* fix: vps-dockermanager declares its real dependency on vps-cockpit

cockpit-packagekit/cockpit-files are Cockpit plugins, and down() tries
`systemctl try-restart cockpit.socket` - none of that means anything
without Cockpit itself already installed, but vps-dockermanager's
package.json only ever declared @tomgrv/vps-common. Verified live (not
just via the test suite): `--only-vps-dockermanager` now auto-enables
vps-cockpit first, and `--down-vps-cockpit` is refused while
vps-dockermanager is still enabled, matching every other dependency edge
in this repo.

Also regenerated every per-folder README.md's body text to the vps-<name>
naming from the previous commit - the file moves/edits there only touched
run.sh/test.bats/package.json and the root README, leaving each feature's
own README still describing itself with the old bare flag/path names
(--only-rancher, common/run.sh, etc.). Root README's "Dependencies between
steps" section and "Removing a feature" table updated to match and to
correct a pre-existing inaccuracy (it said vps-marketplace depends
directly on vps-k3s; it actually depends on vps-rancher, which in turn
depends on vps-k3s - dispatch.sh's auto-enable already resolves this
transitively, verified live).

Regenerated package-lock.json for the new dependency edge.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134CCrDUFyYPUmcbZ2NcKKd

* refactor: remove dispatch.sh, fold orchestration into vps-setup (was vps-summary)

dispatch.sh's flag parsing, interactive menu, feature discovery, and
multi-pass dependency auto-enable now live in vps-setup/run.sh (renamed
from vps-summary/, which already printed the final connection summary -
that's folded in too, printed at the end of every vps-setup run). Root
setup.sh no longer execs anything from this repo - it's now a bulk copy
of tomgrv/scripts' own setup.sh (byte-identical), which only bootstraps
the core zz_* bundle from tomgrv/scripts and stops.

New entry point: bootstrap via setup.sh, then zz_use + run vps-setup as
its own separate step - there is no more single "curl dispatch.sh | sudo
sh" one-liner that does everything end-to-end.

    curl -fsSL https://raw.githubusercontent.com/perspikapps/vps/main/setup.sh | sh
    zz_use perspikapps/vps/vps-setup
    sudo vps-setup

vps-setup isn't itself an installable step (like vps-common, it's
excluded from feature discovery) - since a single zz_use fetch only pulls
its own folder, it detects whether it's running from a full local
checkout or needs to clone/update one itself (VPS_SETUP_REPO_URL/REF/DIR,
same as dispatch.sh's old clone logic) before it can enumerate every
sibling feature folder to orchestrate them.

Also removed the log()/warn()/die() wrapper functions dispatch.sh used to
carry (from before vps-common existed) - vps-setup/run.sh and steps.sh
now call zz_log directly, matching the "no wrapper functions" convention
already enforced repo-wide by tests/test-syntax.bats for every other
run.sh.

Updated: root package.json (dropped "main", renamed the workspace entry),
cloud-init/kairos-vps-setup.yaml (two-step bootstrap+run), every
reference to dispatch.sh, tests/test-syntax.bats, and package-lock.json.
README.md rewrite (still describes the old dispatch.sh flow) follows in
the next commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134CCrDUFyYPUmcbZ2NcKKd

* docs: rewrite README for the vps-setup/no-dispatch.sh flow

Every dispatch.sh reference replaced with vps-setup throughout: the
one-liner, "Running a single step", the interactive menu, "Removing a
feature", "Running from a non-standard branch or fork" (which needed a
real rewrite - the old sudo/piped-curl env-var gotcha doesn't apply once
vps-setup is a real installed command; the new gotcha is sudo's plain env
reset, addressed the same way), Layout, "One folder per feature",
Rancher Marketplace, Troubleshooting, Tests, and Replicating this pattern
in another repo (which now describes copying setup.sh verbatim and
fetching an orchestrator as its own explicit zz_use step, not a
package.json "main"-field auto-exec).

Added a new "Running vps-setup" section up front explaining the two-step
bootstrap+run flow and why vps-setup needs its own full checkout to
orchestrate its sibling folders.

Also fixed two pre-existing inaccuracies while in there: the "Dependencies
between steps" example claimed vps-marketplace depends directly on
vps-k3s (it depends on vps-rancher, which depends on vps-k3s - the
transitive resolution was already real, the docs just didn't say so -
verified live), and "One folder per feature" claimed vps-setup's
package.json reading used sed/awk (verified: it uses jq, via
ensure_jq - the sed/awk description was stale from before that migration
too).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134CCrDUFyYPUmcbZ2NcKKd

* Address Copilot review: fix feature_deps robustness, git fetch option-injection guard, stale dispatch.sh/vps-summary references

feature_deps() now filters vps-common/vps-setup in jq's own select()
instead of piping through grep -v (which could exit 1 on an empty
result under pipefail), and uses if/then instead of a chained
[[ ]] && [[ ]] && printf inside the piped while-read loop, so a single
non-matching dependency can no longer abort the whole loop early under
set -e.

git fetch now passes -- before $REPO_REF to guard against option
injection if a ref were to start with -.

Several per-folder READMEs (vps-system, vps-tailscale, vps-security,
vps-dockermanager, vps-cockpit, vps-rancher, vps-marketplace,
vps-common, vps-setup) still referenced the removed dispatch.sh /
vps-summary from before the vps-setup rename; updated them to describe
the current vps-setup flow.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134CCrDUFyYPUmcbZ2NcKKd

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants