diff --git a/.github/workflows/fleet-converge.yml b/.github/workflows/fleet-converge.yml index de953b1c39c..4a623c87805 100644 --- a/.github/workflows/fleet-converge.yml +++ b/.github/workflows/fleet-converge.yml @@ -293,11 +293,42 @@ jobs: - name: Org Actions credential validation + read-only settings diff id: org_actions_observe run: | + set -euo pipefail + umask 077 + KEY_FILE="$RUNNER_TEMP/org-admin-app-key" + HDR_FILE="$RUNNER_TEMP/org-admin-auth-header" + JWT_FILE="$RUNNER_TEMP/org-admin-app-jwt" + TOKEN_HEADERS="$RUNNER_TEMP/org-admin-token-headers" + TOKEN_BODY="$RUNNER_TEMP/org-admin-token-body" + cleanup() { rm -f "$KEY_FILE" "$HDR_FILE" "$JWT_FILE" "$TOKEN_HEADERS" "$TOKEN_BODY" "$RUNNER_TEMP/org-admin-app-key-sm-body"; unset GUNBC_ORG_ADMIN_TOKEN GH_TOKEN; } + trap cleanup EXIT + printf 'Authorization: Bearer %s\n' "$WIF_ACCESS_TOKEN" > "$HDR_FILE" + SM_BODY="$RUNNER_TEMP/org-admin-app-key-sm-body" + SM_CODE="$(curl -sS -o "$SM_BODY" -w '%{http_code}' -H @"$HDR_FILE" "https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/ci-github-app-private-key/versions/latest:access")" + rm -f "$HDR_FILE" + if [ "$SM_CODE" != 200 ]; then rm -f "$SM_BODY"; echo "OrgAdminAppKeyUnreadable: Secret Manager answered HTTP $SM_CODE for projects/gunbai-secrets/secrets/ci-github-app-private-key/versions/latest to the fleet-cloud-convergence principal. 403 means the version exists and is not readable OR does not exist -- REMEDY: run org_admin_app_key_access_converge_with_supplied_token (gunbc.fleet.org_actions_converge) with a control-plane token to bind roles/secretmanager.secretAccessor, and confirm the version exists. Minting NO token." >&2; exit 1; fi + python3 -c 'import sys,json,base64; sys.stdout.buffer.write(base64.b64decode(json.load(open(sys.argv[1]))["payload"]["data"]))' "$SM_BODY" > "$KEY_FILE" + rm -f "$SM_BODY" + chmod 600 "$KEY_FILE" + NOW=$(date +%s) + b64url() { base64 -w0 | tr '+/' '-_' | tr -d '='; } + JWT_HEADER=$(printf '%s' '{"alg":"RS256","typ":"JWT"}' | b64url) + JWT_PAYLOAD=$(printf '{"iat":%d,"exp":%d,"iss":"%s"}' "$((NOW-60))" "$((NOW+540))" "2653532" | b64url) + JWT_SIG=$(printf '%s.%s' "$JWT_HEADER" "$JWT_PAYLOAD" | openssl dgst -sha256 -sign "$KEY_FILE" | b64url) + rm -f "$KEY_FILE" + printf 'Authorization: Bearer %s.%s.%s\n' "$JWT_HEADER" "$JWT_PAYLOAD" "$JWT_SIG" > "$JWT_FILE" + curl -sS -X POST -H @"$JWT_FILE" -H 'Accept: application/vnd.github+json' -D "$TOKEN_HEADERS" -o "$TOKEN_BODY" "https://api.github.com/app/installations/104134109/access_tokens" + rm -f "$JWT_FILE" + if ! head -n1 "$TOKEN_HEADERS" | grep -q ' 201 '; then echo "OrgAdminInstallationTokenRefused: GitHub answered $(head -n1 "$TOKEN_HEADERS" | tr -d '\r') for the gunbai-ci installation; a revoked key, a removed installation, or a missing Self-hosted-runners permission each look like this -- inspect the App's installation on the organization" >&2; exit 1; fi + GUNBC_ORG_ADMIN_TOKEN="$(python3 -c 'import sys,json; print(json.load(open(sys.argv[1]))["token"])' "$TOKEN_BODY")" + rm -f "$TOKEN_HEADERS" "$TOKEN_BODY" + export GUNBC_ORG_ADMIN_TOKEN + export GH_TOKEN="$GUNBC_ORG_ADMIN_TOKEN" + echo "org-admin: installation token minted in-run for app gunbai-ci installation 104134109 (one-hour lifetime; key wiped; token held in this step's environment only)" ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) "$ROOT/target/release/gunbc" run --source-root "$ROOT/dag" --source-root "$ROOT/src/v2" --entry dag/gunbc/fleet/org_actions_converge.dag --function org_actions_converge_wet env: - GUNBC_ORG_ADMIN_TOKEN: ${{ secrets.GUNBC_ORG_ADMIN_TOKEN }} - GH_TOKEN: ${{ secrets.GUNBC_ORG_ADMIN_TOKEN }} + WIF_ACCESS_TOKEN: ${{ steps.wif_auth.outputs.access_token }} if: github.event.inputs.mode == 'org_actions_observe' timeout-minutes: 5 - name: Upload org Actions credential validation receipt diff --git a/dag/extdeps/github/actions_runner.dag b/dag/extdeps/github/actions_runner.dag index 1bfd27659e2..4eb1bc0a1a6 100644 --- a/dag/extdeps/github/actions_runner.dag +++ b/dag/extdeps/github/actions_runner.dag @@ -18,6 +18,11 @@ import extdeps.toolchain.types { Wasm32, } import std.types { NonEmptyStr, String, List } +import extdeps.languages.json.parse { + parse_json_document, JsonDocumentParsed, JsonDocumentUnreadable, json_document_gap_text, + json_object_unique_member, JsonMemberFound, JsonMemberAbsent, JsonMemberDuplicated, JsonMemberNotAnObject, +} +import extdeps.languages.json.emit { JsonString } import std.disposition { Disposition, Scaffold, SingleAuthority } import std.decl_ref { DeclarationRef, WholeDeclaration } @@ -134,6 +139,48 @@ data actions_runner_registration_file_name: String = ".runner" data actions_runner_cache_dir: String = "/opt/actions-runner-cache" +// THE REGISTRATION FILE SAYS WHETHER THE REGISTRATION CAN OUTLIVE ONE JOB. `.runner` is the JSON +// `config.sh` writes; its `Ephemeral` member is the string "True" when the runner was configured +// with `--ephemeral`. Upstream semantics (docs.github.com, "Autoscaling with self-hosted runners", +// Using ephemeral runners): an ephemeral runner is automatically unregistered from GitHub after it +// completes one job, and is not re-registered by restarting the process. So an ephemeral +// registration file beside a unit that is neither active nor enabled records a registration GitHub +// has already dropped; nothing remains to `config.sh remove`, and no credential is needed to know +// it. A persistent registration (`Ephemeral` absent or "False") stays registered org-side until +// removed, which needs an org-scoped removal token this repository does not hold, so a persistent +// registration is NOT decidable from the file alone. +type ActionsRunnerRegistration + = ActionsRunnerRegistrationPersistent + | ActionsRunnerRegistrationEphemeral + +type ActionsRunnerRegistrationRead + = ActionsRunnerRegistrationDecoded { registration: ActionsRunnerRegistration } + | ActionsRunnerRegistrationUnreadable { reason: String } + +data actions_runner_registration_ephemeral_member: String = "Ephemeral" + +fn actions_runner_registration_decode(raw: String) -> ActionsRunnerRegistrationRead { + match parse_json_document(s: raw) { + JsonDocumentUnreadable { gap } => + ActionsRunnerRegistrationUnreadable { reason: concat(".runner is ", json_document_gap_text(gap: gap)) } + JsonDocumentParsed { value: doc } => + match json_object_unique_member(v: doc, key: actions_runner_registration_ephemeral_member) { + JsonMemberFound { value: JsonString { value: flag } } => + if flag == "True" { + ActionsRunnerRegistrationDecoded { registration: ActionsRunnerRegistrationEphemeral } + } else if flag == "False" { + ActionsRunnerRegistrationDecoded { registration: ActionsRunnerRegistrationPersistent } + } else { + ActionsRunnerRegistrationUnreadable { reason: concat(".runner Ephemeral is outside True/False: ", flag) } + } + JsonMemberFound { value: _ } => ActionsRunnerRegistrationUnreadable { reason: ".runner Ephemeral is not a string" } + JsonMemberAbsent => ActionsRunnerRegistrationDecoded { registration: ActionsRunnerRegistrationPersistent } + JsonMemberDuplicated { count: _ } => ActionsRunnerRegistrationUnreadable { reason: ".runner Ephemeral is duplicated" } + JsonMemberNotAnObject => ActionsRunnerRegistrationUnreadable { reason: ".runner is not a JSON object" } + } + } +} + data actions_runner_slot_extract_script_scaffold: Disposition = Scaffold { dissolves_to: SingleAuthority, bind: DeclarationRef { diff --git a/dag/extdeps/github/org_admin_auth.dag b/dag/extdeps/github/org_admin_auth.dag index a574f328b59..ec0ee5a484c 100644 --- a/dag/extdeps/github/org_admin_auth.dag +++ b/dag/extdeps/github/org_admin_auth.dag @@ -1,6 +1,6 @@ module extdeps.github.org_admin_auth -import std.types { NonEmptyStr, Timestamp } +import std.types { NonEmptyStr, Timestamp, String, Int } import std.decl_ref { DeclarationRef, WholeDeclaration } import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef } import extdeps.uri { Uri, Https } @@ -23,6 +23,21 @@ data installation_token_citation: ExternalAuthority = ExternalAuthority { uri: Uri { scheme: Https, locator: "docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app" } } +// The installation access token endpoint and the App JWT bounds are facts of GitHub's API +// (installation_token_citation): the JWT is signed RS256 with the App's private key, its `iat` may +// be backdated up to 60 s for clock skew, its `exp` may be at most 10 minutes after issue, and the +// POST returns a token valid for one hour. Modeled here so the workflow step that mints one names +// the endpoint through the citation rather than spelling a URL. +data github_app_installation_access_token_url_prefix: String = "https://api.github.com/app/installations/" + +fn github_app_installation_access_token_url(installation_id: Int) -> String { + join([github_app_installation_access_token_url_prefix, to_string(installation_id), "/access_tokens"], "") +} + +data github_app_jwt_backdate_seconds: Int = 60 + +data github_app_jwt_lifetime_seconds: Int = 540 + data oauth_flow_citation: ExternalAuthority = ExternalAuthority { uri: Uri { scheme: Https, locator: "docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps" } } diff --git a/dag/gunbc/ci/ci_spec.dag b/dag/gunbc/ci/ci_spec.dag index 7a53c96d5df..9d2eeb28021 100644 --- a/dag/gunbc/ci/ci_spec.dag +++ b/dag/gunbc/ci/ci_spec.dag @@ -1,6 +1,12 @@ module gunbc.ci_spec import gunbc.ledger_row_coherence { heal_repair_declaration_artifact_path } import extdeps.cloud.gcp.secret_ref { secret_ref_access_url, secret_ref_version_resource } +import gunbc.auth.github_apps { gunbai_ci_declared, gunbai_ci_app_pem_secret, gunbai_ci_declared_installation_id } +import extdeps.github.org_admin_auth { + github_app_installation_access_token_url, + github_app_jwt_backdate_seconds, + github_app_jwt_lifetime_seconds, +} import gunbc.spark.credential_workflow { spark_administrator_password_secret_ref } import gunbc.spark.grant_privileged_operation { spark_grant_credential_path_env } import gunbc.spark.bootstrap_provision { spark_bootstrap_cloud_principal_member } @@ -1362,8 +1368,116 @@ fn gunbc_ci_fleet_converge_apply_invoke() -> String { ) } +// THE ORG-ADMIN CREDENTIAL IS MINTED IN-RUN FROM THE App KEY THE FLEET ALREADY HOLDS; NO HUMAN +// TOKEN EXISTS. The interim in docs/plans/org-admin-credential-acquisition.md asked the operator to +// mint a fine-grained PAT and paste it into an Actions secret. That step was never taken, and it +// was never necessary: the gunbai-ci GitHub App (gunbc.auth.github_apps gunbai_ci_declared) is +// installed on the organization, its private key sits in Secret Manager behind the same WIF read +// the fleet key uses, and the runner installer already mints registration tokens with it. So this +// prelude follows the fleet-key lifecycle exactly -- WIF token by env, the SecretRef's own access +// URL, a 0600 file under RUNNER_TEMP, a trap armed before the key touches disk -- then signs a +// ten-minute RS256 JWT with openssl, exchanges it for a one-hour installation token +// (extdeps.github.org_admin_auth installation_token_citation), wipes the key, and exports the token +// into THIS step's environment only. Nothing reaches GITHUB_ENV; the trap unsets it on exit. +// +// THE HTTP STATUS IS CLASSIFIED, NOT COLLAPSED (the lesson gunbc_ci_fleet_key_agent_prelude's +// administrator sibling records): the mint POST writes its headers and body to files, and anything +// but 201 refuses with the status line, so a revoked key, a removed installation and a missing +// permission each surface as what they are rather than as a JSON decode traceback. +fn gunbc_ci_org_admin_app_token_prelude() -> List { + [ + Do { run: ci_retry_body_run(command: "set -euo pipefail") }, + Do { run: ci_retry_body_run(command: "umask 077") }, + Do { run: ci_retry_body_run(command: "KEY_FILE=\"$RUNNER_TEMP/org-admin-app-key\"") }, + Do { run: ci_retry_body_run(command: "HDR_FILE=\"$RUNNER_TEMP/org-admin-auth-header\"") }, + Do { run: ci_retry_body_run(command: "JWT_FILE=\"$RUNNER_TEMP/org-admin-app-jwt\"") }, + Do { run: ci_retry_body_run(command: "TOKEN_HEADERS=\"$RUNNER_TEMP/org-admin-token-headers\"") }, + Do { run: ci_retry_body_run(command: "TOKEN_BODY=\"$RUNNER_TEMP/org-admin-token-body\"") }, + Do { + run: ci_retry_body_run( + command: "cleanup() { rm -f \"$KEY_FILE\" \"$HDR_FILE\" \"$JWT_FILE\" \"$TOKEN_HEADERS\" \"$TOKEN_BODY\" \"$RUNNER_TEMP/org-admin-app-key-sm-body\"; unset GUNBC_ORG_ADMIN_TOKEN GH_TOKEN; }" + ) + }, + Do { run: ci_retry_body_run(command: "trap cleanup EXIT") }, + Do { run: ci_retry_body_run(command: gunbc_ci_auth_header_file_write_command()) }, + Do { run: ci_retry_body_run(command: "SM_BODY=\"$RUNNER_TEMP/org-admin-app-key-sm-body\"") }, + Do { + run: ci_retry_body_run( + command: join([ + "SM_CODE=\"$(curl -sS -o \"$SM_BODY\" -w '%\{http_code}' -H @\"$HDR_FILE\" \"", + secret_ref_access_url(ref: gunbai_ci_app_pem_secret), + "\")\"" + ], "") + ) + }, + Do { run: ci_retry_body_run(command: "rm -f \"$HDR_FILE\"") }, + Do { + run: ci_retry_body_run( + command: join([ + "if [ \"$SM_CODE\" != 200 ]; then rm -f \"$SM_BODY\"; echo \"OrgAdminAppKeyUnreadable: Secret Manager answered HTTP $SM_CODE for ", + secret_ref_version_resource(ref: gunbai_ci_app_pem_secret), + " to the fleet-cloud-convergence principal. 403 means the version exists and is not readable OR does not exist -- REMEDY: run org_admin_app_key_access_converge_with_supplied_token (gunbc.fleet.org_actions_converge) with a control-plane token to bind roles/secretmanager.secretAccessor, and confirm the version exists. Minting NO token.\" >&2; exit 1; fi" + ], "") + ) + }, + Do { run: ci_retry_body_run(command: "python3 -c 'import sys,json,base64; sys.stdout.buffer.write(base64.b64decode(json.load(open(sys.argv[1]))[\"payload\"][\"data\"]))' \"$SM_BODY\" > \"$KEY_FILE\"") }, + Do { run: ci_retry_body_run(command: "rm -f \"$SM_BODY\"") }, + Do { run: ci_retry_body_run(command: "chmod 600 \"$KEY_FILE\"") }, + Do { run: ci_retry_body_run(command: "NOW=$(date +%s)") }, + Do { run: ci_retry_body_run(command: "b64url() { base64 -w0 | tr '+/' '-_' | tr -d '='; }") }, + Do { run: ci_retry_body_run(command: "JWT_HEADER=$(printf '%s' '\{\"alg\":\"RS256\",\"typ\":\"JWT\"}' | b64url)") }, + Do { + run: ci_retry_body_run( + command: join([ + "JWT_PAYLOAD=$(printf '\{\"iat\":%d,\"exp\":%d,\"iss\":\"%s\"}' \"$((NOW-", + to_string(github_app_jwt_backdate_seconds), + "))\" \"$((NOW+", + to_string(github_app_jwt_lifetime_seconds), + "))\" \"", + to_string(gunbai_ci_declared.app_id.value), + "\" | b64url)" + ], "") + ) + }, + Do { run: ci_retry_body_run(command: "JWT_SIG=$(printf '%s.%s' \"$JWT_HEADER\" \"$JWT_PAYLOAD\" | openssl dgst -sha256 -sign \"$KEY_FILE\" | b64url)") }, + Do { run: ci_retry_body_run(command: "rm -f \"$KEY_FILE\"") }, + Do { run: ci_retry_body_run(command: "printf 'Authorization: Bearer %s.%s.%s\\n' \"$JWT_HEADER\" \"$JWT_PAYLOAD\" \"$JWT_SIG\" > \"$JWT_FILE\"") }, + Do { + run: ci_retry_body_run( + command: join([ + "curl -sS -X POST -H @\"$JWT_FILE\" -H 'Accept: application/vnd.github+json' -D \"$TOKEN_HEADERS\" -o \"$TOKEN_BODY\" \"", + github_app_installation_access_token_url(installation_id: gunbai_ci_declared_installation_id.value), + "\"" + ], "") + ) + }, + Do { run: ci_retry_body_run(command: "rm -f \"$JWT_FILE\"") }, + Do { + run: ci_retry_body_run( + command: "if ! head -n1 \"$TOKEN_HEADERS\" | grep -q ' 201 '; then echo \"OrgAdminInstallationTokenRefused: GitHub answered $(head -n1 \"$TOKEN_HEADERS\" | tr -d '\\r') for the gunbai-ci installation; a revoked key, a removed installation, or a missing Self-hosted-runners permission each look like this -- inspect the App's installation on the organization\" >&2; exit 1; fi" + ) + }, + Do { run: ci_retry_body_run(command: "GUNBC_ORG_ADMIN_TOKEN=\"$(python3 -c 'import sys,json; print(json.load(open(sys.argv[1]))[\"token\"])' \"$TOKEN_BODY\")\"") }, + Do { run: ci_retry_body_run(command: "rm -f \"$TOKEN_HEADERS\" \"$TOKEN_BODY\"") }, + Do { run: ci_retry_body_run(command: "export GUNBC_ORG_ADMIN_TOKEN") }, + Do { run: ci_retry_body_run(command: "export GH_TOKEN=\"$GUNBC_ORG_ADMIN_TOKEN\"") }, + Do { + run: ci_retry_body_run( + command: join([ + "echo \"org-admin: installation token minted in-run for app ", + gunbai_ci_declared.slug as String, + " installation ", + to_string(gunbai_ci_declared_installation_id.value), + " (one-hour lifetime; key wiped; token held in this step's environment only)\"" + ], "") + ) + } + ] +} + fn gunbc_ci_org_actions_converge_invoke() -> String { - gunbc_run_step_script( + gunbc_run_step_script_with_prelude( + prelude: gunbc_ci_org_admin_app_token_prelude(), source_roots: witness_layer_roots, entry: gunbc_ci_org_actions_converge_target.entry, function: gunbc_ci_org_actions_converge_target.function, diff --git a/dag/gunbc/executor_privileged_operation.dag b/dag/gunbc/executor_privileged_operation.dag index b052a7b7431..357f5add778 100644 --- a/dag/gunbc/executor_privileged_operation.dag +++ b/dag/gunbc/executor_privileged_operation.dag @@ -173,6 +173,18 @@ fn executor_privileged_operations_sudoers_body( // longer generated file and removes the traversal entirely. data executor_managed_directory_mode: NonEmptyStr = "755" as NonEmptyStr +fn runner_slot_install_operations( + slot_dirs: List, + slot_owner: NonEmptyStr, +) -> List { + map(slot_dirs, d => EnsureOwnedDirectory { + path: d, + owner: slot_owner, + group: slot_owner, + mode: executor_managed_directory_mode, + }) +} + fn runner_slot_privileged_operations( slot_dirs: List, slot_owner: NonEmptyStr, diff --git a/dag/gunbc/fleet/fleet_converge_plan_cli.dag b/dag/gunbc/fleet/fleet_converge_plan_cli.dag index 8f9ed349d6b..b3f23aeb671 100644 --- a/dag/gunbc/fleet/fleet_converge_plan_cli.dag +++ b/dag/gunbc/fleet/fleet_converge_plan_cli.dag @@ -29,6 +29,7 @@ import gunbc.runner_slot_provision { runner_slot_name_in_host_grammar, RunnerSlotProvenance, SlotProvenanceObserved, + RunnerSlotRegistration, SlotUnregistered, SlotRegisteredPersistent, SlotRegisteredEphemeral, SlotRegistrationUnreadable, RunnerSlotObservation, RunnerSlotMember, RunnerSlotsObserved, @@ -36,7 +37,12 @@ import gunbc.runner_slot_provision { observe_runner_slot_members_wet, } import product.placement_supply { HostIdentity } -import extdeps.github.actions_runner { actions_runner_registration_file_name } +import extdeps.github.actions_runner { + actions_runner_registration_file_name, + actions_runner_registration_decode, + ActionsRunnerRegistrationDecoded, ActionsRunnerRegistrationUnreadable, + ActionsRunnerRegistrationPersistent, ActionsRunnerRegistrationEphemeral, +} import gunbc.runner_unit { runner_unit_name_of_registration } import gunbc.fabric_cell_observation_admission { admit_fabric_cell_probe, FabricCellObservationDecision } import gunbc.fabric_cell_acquire { fabric_cell_probe_wet } @@ -583,7 +589,18 @@ func write_fleet_converge_plan_artifact_wet(artifact: FleetConvergePlanArtifact) transport: LocalExec, ) } else { - exit_failure(reason: "fleet_converge_plan: artifact write refused") + exit_failure(reason: fleet_converge_plan_artifact_write_refusal(rows: [ + fleet_converge_write_refusal_row(path: fleet_converge_plan_body_path, success: w_plan.success, error: w_plan.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_baseline_hex_path, success: w_base.success, error: w_base.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_content_hash_path, success: w_hash.success, error: w_hash.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_apply_shell_path, success: w_apply.success, error: w_apply.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_subject_host_path, success: w_host.success, error: w_host.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_generation_path, success: w_gen.success, error: w_gen.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_prior_generation_path, success: w_prior.success, error: w_prior.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_member_set_fp_path, success: w_member_fp.success, error: w_member_fp.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_subject_scope_path, success: w_scope.success, error: w_scope.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_spark_typed_actions_wire_path, success: w_spark_wire.success, error: w_spark_wire.error), + ])) } } } @@ -994,7 +1011,18 @@ func fleet_converge_plan_scoped_bound_wet( ) } } else { - exit_failure(reason: "fleet_converge_plan: artifact write refused") + exit_failure(reason: fleet_converge_plan_artifact_write_refusal(rows: [ + fleet_converge_write_refusal_row(path: fleet_converge_plan_body_path, success: w_plan.success, error: w_plan.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_baseline_hex_path, success: w_base.success, error: w_base.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_content_hash_path, success: w_hash.success, error: w_hash.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_apply_shell_path, success: w_apply.success, error: w_apply.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_subject_host_path, success: w_host.success, error: w_host.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_generation_path, success: w_gen.success, error: w_gen.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_prior_generation_path, success: w_prior.success, error: w_prior.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_member_set_fp_path, success: w_member_fp.success, error: w_member_fp.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_subject_scope_path, success: w_scope.success, error: w_scope.error), + fleet_converge_write_refusal_row(path: fleet_converge_plan_spark_typed_actions_wire_path, success: w_spark_wire.success, error: w_spark_wire.error), + ])) } } } @@ -1581,6 +1609,38 @@ func fleet_converge_local_apply_wet(plan_run_id: String, plan_hash: String) -> P // unit's ActiveState and its UnitFileState -- and a name outside the grammar gets none, because no // unit or registration is defined for it. Any unreadable unit property refuses the WHOLE // observation: a roster in which one tree's liveness was guessed could remove a live runner. +// The registration KIND comes from the file's own bytes, read only when the file is present; an +// unreadable or undecodable file is carried as its own arm so ownership refuses on it rather than +// defaulting either way. +fn observe_runner_slot_registration_wet(present: Bool, path: String) -> RunnerSlotRegistration { + if !present { + SlotUnregistered + } else { + let read = Filesystem.Read(path: path) + if !read.success { + SlotRegistrationUnreadable { reason: join([".runner present but unreadable: ", read.error], "") } + } else { + match actions_runner_registration_decode(raw: read.content) { + ActionsRunnerRegistrationUnreadable { reason } => SlotRegistrationUnreadable { reason: reason } + ActionsRunnerRegistrationDecoded { registration: ActionsRunnerRegistrationEphemeral } => SlotRegisteredEphemeral + ActionsRunnerRegistrationDecoded { registration: ActionsRunnerRegistrationPersistent } => SlotRegisteredPersistent + } + } + } +} + +// A refused artifact write names WHICH path refused and why. The srv2 plan run of 2026-09-04 22:42 +// refused with the bare sentence and the cause -- /tmp/fleet-converge-plan owned by another +// principal from a local plan run -- had to be found by hand on the host. The shared literal dir +// is the underlying defect and is declared, not solved, here. +fn fleet_converge_write_refusal_row(path: String, success: Bool, error: String) -> List { + if success { [] } else { [join([path, ": ", error], "")] } +} + +fn fleet_converge_plan_artifact_write_refusal(rows: List>) -> String { + join(["fleet_converge_plan: artifact write refused: ", join(flat_map(rows, r => r), "; ")], "") +} + type RunnerSlotProvenanceRead = SlotProvenanceRead { member: RunnerSlotMember } | SlotProvenanceRefused { reason: String } @@ -1593,14 +1653,16 @@ func observe_runner_slot_provenance_wet(host: NonEmptyStr, member: RunnerSlotMem SlotProvenanceRead { member: runner_slot_member_observed( instance_name: name as String, - provenance: SlotProvenanceObserved { in_host_grammar: false, registered: false, unit_active: false, unit_enabled: false }, + provenance: SlotProvenanceObserved { in_host_grammar: false, registration: SlotUnregistered, unit_active: false, unit_enabled: false }, ), } } else { + let registration_path = join([member.slot_dir as String, "/", actions_runner_registration_file_name], "") let registered = process_outcome_admitted(outcome: run_shell_command_capture( - command: test_regular_file_command(path: join([member.slot_dir as String, "/", actions_runner_registration_file_name], "")), + command: test_regular_file_command(path: registration_path), transport: LocalExec, )) + let registration = observe_runner_slot_registration_wet(present: registered, path: registration_path) let unit = runner_unit_name_of_registration(registration_name: name) match read_unit_property(unit: unit, property: ActiveState) { UnitPropertyUnreadable { unit: u, property: p } => @@ -1615,7 +1677,7 @@ func observe_runner_slot_provenance_wet(host: NonEmptyStr, member: RunnerSlotMem instance_name: name as String, provenance: SlotProvenanceObserved { in_host_grammar: true, - registered: registered, + registration: registration, unit_active: active_state == "active", unit_enabled: file_state == "enabled", }, diff --git a/dag/gunbc/fleet/fleet_converge_workflow.dag b/dag/gunbc/fleet/fleet_converge_workflow.dag index edb7b476218..650262788e7 100644 --- a/dag/gunbc/fleet/fleet_converge_workflow.dag +++ b/dag/gunbc/fleet/fleet_converge_workflow.dag @@ -350,8 +350,7 @@ fn fleet_converge_org_actions_step() -> Step { run: gunbc_ci_org_actions_converge_invoke(), shell: none, env: Present { value: [ - kv(key: "GUNBC_ORG_ADMIN_TOKEN", value: yaml_string(s: "${{ secrets.GUNBC_ORG_ADMIN_TOKEN }}")), - kv(key: "GH_TOKEN", value: yaml_string(s: "${{ secrets.GUNBC_ORG_ADMIN_TOKEN }}")), + kv(key: "WIF_ACCESS_TOKEN", value: yaml_string(s: "${{ steps.wif_auth.outputs.access_token }}")), ] }, working_directory: none, if_condition: Present { value: fleet_converge_org_actions_step_if }, diff --git a/dag/gunbc/fleet/fleet_main_revision.dag b/dag/gunbc/fleet/fleet_main_revision.dag index 31aed8f5319..552f4e62e22 100644 --- a/dag/gunbc/fleet/fleet_main_revision.dag +++ b/dag/gunbc/fleet/fleet_main_revision.dag @@ -21,6 +21,7 @@ import gunbc.live_deploy.revision { DeployedIsAncestor, CandidateIsAncestor, UnrelatedHistories, + DivergedHistories, RelationUnverifiable, } import gunbc.fleet_revision_acceptance { AcceptedFleetRevision } @@ -285,6 +286,7 @@ fn relation_from_merge_base_oid( type FleetDesiredAdvanceRefusal = AdvanceExpectationUnavailable { cause: FleetDesiredExpectationRefusal } | AdvanceHistoriesUnrelated { current: GitObjectId, accepted: GitObjectId } + | AdvanceHistoriesDiverged { current: GitObjectId, accepted: GitObjectId } | AdvanceRelationUnobservable { current: GitObjectId, accepted: GitObjectId, cause: NonEmptyStr } | InitialFleetDesiredCreationNotAuthorized { accepted: GitObjectId } | AdvanceRelationIsAboutAnotherPair { @@ -398,6 +400,13 @@ fn fleet_desired_advance_refusal_message(cause: FleetDesiredAdvanceRefusal) -> S ", accepted ", git_object_id_wire_hex(oid: acc) as String, ") -- advancing would move the ref sideways", ], "") + AdvanceHistoriesDiverged { current: cur, accepted: acc } => + join([ + "the revisions have diverged: they share an ancestor but neither is an ancestor of the other (current ", + git_object_id_wire_hex(oid: cur) as String, + ", accepted ", git_object_id_wire_hex(oid: acc) as String, + ") -- advancing would move the ref sideways", + ], "") AdvanceRelationUnobservable { current: cur, accepted: acc, cause: c } => join([ "the ancestry of ", git_object_id_wire_hex(oid: cur) as String, @@ -536,6 +545,10 @@ fn classify_admitted_relation( FleetDesiredAdvanceRefused { cause: AdvanceHistoriesUnrelated { current: current, accepted: accepted.revision }, } + DivergedHistories => + FleetDesiredAdvanceRefused { + cause: AdvanceHistoriesDiverged { current: current, accepted: accepted.revision }, + } RelationUnverifiable { cause: c } => FleetDesiredAdvanceRefused { cause: AdvanceRelationUnobservable { diff --git a/dag/gunbc/fleet/org_actions_converge.dag b/dag/gunbc/fleet/org_actions_converge.dag index 2365444382a..560493804e0 100644 --- a/dag/gunbc/fleet/org_actions_converge.dag +++ b/dag/gunbc/fleet/org_actions_converge.dag @@ -15,6 +15,8 @@ import extdeps.languages.json.parse { JsonMemberNotAnObject } import extdeps.shell.credentials { env_credential } +import gunbc.auth.github_apps { gunbai_ci_app_pem_secret } +import gunbc.spark.secret_access_ensure { secret_access_ensure_for, read_supplied_access_token, SuppliedTokenReady, SuppliedTokenUnavailable, OperatorSuppliedToken } import extdeps.github.org_admin_auth { RunnerGroupsRead, RunnerGroupRepositoriesRead } import extdeps.filesystem.filesystem_io { Filesystem } import gunbc.clock_read { clock_now_probed_at_or_unknown } @@ -228,7 +230,7 @@ fn completed_groups(reads: List) -> List func org_actions_converge_wet() -> ProcessExit { let credential = env_credential(env_var: org_admin_token_env_name) match credential.token { - Absent => exit_failure(reason: "OrgActionsCredentialMissing: GUNBC_ORG_ADMIN_TOKEN is absent") + Absent => exit_failure(reason: "OrgActionsCredentialMissing: GUNBC_ORG_ADMIN_TOKEN is absent -- the org-actions workflow step mints it in-run from the gunbai-ci App key (gunbc.ci_spec gunbc_ci_org_admin_app_token_prelude); outside that step there is no credential to read") Present { value: _ } => { let listed = github.CliOrgRunnerGroups.ListRunnerGroupsJson(org: org_actions_organization) if !listed.success { @@ -272,3 +274,19 @@ func org_actions_converge_wet() -> ProcessExit { } } } + +// THE ACCESSOR BINDING FOR THE App KEY IS A CONVERGE, RUN BY THE CONTROL-PLANE PRINCIPAL. The +// workload principal (fleet-cloud-convergence) may read secrets it is bound to and may not bind +// itself, so this entry takes an operator-supplied access token (GUNBC_GCP_ACCESS_TOKEN_FILE, the +// same channel gunbc.spark.secret_access_ensure uses) and reconciles roles/secretmanager.secretAccessor +// on ci-github-app-private-key for that principal: get policy, delta, set policy, read back the +// grant. Once it has run, the org-actions step's in-run mint stops answering 403. +func org_admin_app_key_access_converge_with_supplied_token() -> ProcessExit + uses net: Network +{ + match read_supplied_access_token() { + SuppliedTokenUnavailable { cause: c } => exit_failure(reason: c as String) + SuppliedTokenReady { token: t } => + secret_access_ensure_for(target: gunbai_ci_app_pem_secret, token_source: OperatorSuppliedToken { token: t }) + } +} diff --git a/dag/gunbc/live_deploy/revision.dag b/dag/gunbc/live_deploy/revision.dag index 423567cbe89..7b9791cc441 100644 --- a/dag/gunbc/live_deploy/revision.dag +++ b/dag/gunbc/live_deploy/revision.dag @@ -69,6 +69,7 @@ type DeployRevisionRelation | DeployedIsAncestor | CandidateIsAncestor | UnrelatedHistories + | DivergedHistories | RelationUnverifiable { cause: NonEmptyStr } type DeployRevisionVerdict @@ -86,6 +87,10 @@ fn classify_deploy_revision(relation: DeployRevisionRelation) -> DeployRevisionV RevisionRefused { cause: "deployed and candidate revisions share no ancestry (force-push, rewritten history, or foreign repository) — refusing rather than guessing which is newer" as NonEmptyStr } + DivergedHistories => + RevisionRefused { + cause: "deployed and candidate revisions have diverged: they share an ancestor but neither is an ancestor of the other, so installing the candidate would move the running release sideways — refusing rather than guessing which changes would be lost" as NonEmptyStr, + } RelationUnverifiable { cause: why } => RevisionRefused { cause: why } } } @@ -156,22 +161,33 @@ fn revision_relation_from_ancestry_facts( if deployed_is_ancestor_of_candidate { if candidate_is_ancestor_of_deployed { SameRevision } else { DeployedIsAncestor } } else { - if candidate_is_ancestor_of_deployed { CandidateIsAncestor } else { UnrelatedHistories } + if candidate_is_ancestor_of_deployed { CandidateIsAncestor } else { DivergedHistories } } } } +// DIVERGED IS NOT UNRELATED. The srv1 deploy of 2026-09-04 refused with 'share no ancestry' when the +// running release was a sibling of the candidate with a merge base two commits back: the neither- +// ancestor arm had been folded into the no-common-ancestor arm, so the refusal named a force-push +// that never happened. revision_relation_from_ancestry_facts is reached only once a merge base was +// REPORTED (relation_from_merge_base_oid), so 'neither' there means diverged; a merge-base exit of 1 +// reaches UnrelatedHistories on its own path. Two bare is-ancestor probes cannot tell the two apart, +// so the probe fold below says so rather than picking one. fn deploy_revision_relation_from_probes( deployed: CommitSha, candidate: CommitSha, deployed_is_ancestor_of_candidate: Bool, candidate_is_ancestor_of_deployed: Bool, ) -> DeployRevisionRelation { - revision_relation_from_ancestry_facts( - same: deployed == candidate, - deployed_is_ancestor_of_candidate: deployed_is_ancestor_of_candidate, - candidate_is_ancestor_of_deployed: candidate_is_ancestor_of_deployed, - ) + if deployed == candidate { + SameRevision + } else if deployed_is_ancestor_of_candidate { + DeployedIsAncestor + } else if candidate_is_ancestor_of_deployed { + CandidateIsAncestor + } else { + RelationUnverifiable { cause: "two is-ancestor probes answered no in both directions, which cannot distinguish diverged histories from unrelated ones; a merge-base observation decides" as NonEmptyStr } + } } // The deployed-revision record lives OUTSIDE the directory the deploy rsyncs into, and that diff --git a/dag/gunbc/runner/runner_host_grants.dag b/dag/gunbc/runner/runner_host_grants.dag index 8001a1cfbe7..76082b74671 100644 --- a/dag/gunbc/runner/runner_host_grants.dag +++ b/dag/gunbc/runner/runner_host_grants.dag @@ -21,7 +21,7 @@ import gunbc.executor_privileged_operation { runner_slot_privileged_operations, } import extdeps.systemd.unit_file { SystemdSliceDirective, systemd_slice_directive_line } -import gunbc.build_cache_instance { RunnerSlotIdentity } +import gunbc.build_cache_instance { RunnerSlotIdentity, runner_slot_instance_name } import gunbc.runner_slot_allocation { fabric_execution_slot_identities } import gunbc.fabric_cell_converge { fabric_cell_base_dir, @@ -44,7 +44,9 @@ import gunbc.fabric_allocation_store_substrate { fabric_allocation_store_ensure_effect, } import gunbc.runner_host_deploy { RunnerHostDeploy } -import gunbc.runner_slot_provision { desired_runner_slot_members } +import product.placement_supply { HostIdentity } +import gunbc.runner_slot_provision { desired_runner_slot_members, runner_slot_instance_dir } +import gunbc.runner_host_deploy { runner_index_seq } import gunbc.runner_unit { runner_unit_name_of_registration } // THE MANAGED STATE DIRECTORY, DECLARED ONCE. It is currently spelled THREE times in @@ -249,10 +251,33 @@ fn fabric_allocation_store_privileged_operations(deploy: RunnerHostDeploy) -> Li } } +// THE TEARDOWN GRANT ENROLLS THE SLOT GRAMMAR, NOT THE CURRENT WIDTH. Sudoers is derived from desired +// state, but the trees a reconcile removes are RETIRED ones -- by definition outside the desired +// width -- so a grant that named only desired dirs authorized exactly the removals that never +// happen and refused every one that does (runner_slot_teardown_grant_note). The removal grant now +// names every slot dir the host's grammar can render up to a declared index bound, still one exact +// path per line with no wildcard (the `..` traversal argument in executor_privileged_operation +// stands). The bound is a policy budget: the widest committed width in the fleet is 21 and the +// retired incarnations reclaimed on srv2 on 2026-09-04 ran well past it, so 64 covers every tree +// that exists with headroom for growth, at 64 sudoers lines per host. A retired tree above the bound refuses at apply exactly as +// before, and the witness pins the bound above every committed width so a width change cannot +// silently outgrow its own teardown. +data runner_slot_teardown_grant_index_bound: Int = 64 + +fn runner_slot_grammar_dirs_to_bound(host: HostIdentity, bound: Int) -> List { + map(runner_index_seq(n: bound), i => + runner_slot_instance_dir(instance_name: runner_slot_instance_name(slot: RunnerSlotIdentity { host: host, slot_index: i }) as NonEmptyStr)) +} + +fn runner_slot_teardown_grant_operations(deploy: RunnerHostDeploy) -> List { + map(runner_slot_grammar_dirs_to_bound(host: deploy.host_label as HostIdentity, bound: runner_slot_teardown_grant_index_bound), d => RemoveDirectoryTree { path: d }) +} + fn runner_host_privileged_operations(deploy: RunnerHostDeploy) -> List { let job = deploy.job_user let slot_dirs = map(desired_runner_slot_members(deploy: deploy), m => m.slot_dir) - runner_slot_privileged_operations(slot_dirs: slot_dirs, slot_owner: job) + runner_slot_teardown_grant_operations(deploy: deploy) + + runner_slot_install_operations(slot_dirs: slot_dirs, slot_owner: job) + runner_slot_activation_operations(deploy: deploy) + fabric_cell_privileged_operations(deploy: deploy) + fabric_allocation_store_privileged_operations(deploy: deploy) diff --git a/dag/gunbc/runner/runner_slot_provision.dag b/dag/gunbc/runner/runner_slot_provision.dag index f75f672e1b4..d59b382a8dd 100644 --- a/dag/gunbc/runner/runner_slot_provision.dag +++ b/dag/gunbc/runner/runner_slot_provision.dag @@ -69,21 +69,54 @@ type RunnerSlotMemberAt = NonEmptyStr // out-of-roster tree refused OwnershipUnknown forever and 45 dead runner trees sat on srv2 at two // gigabytes each. A tree is a retired incarnation of a gunbc-managed slot when four observed facts // agree: its name is in this host's slot grammar (the allocation authority's own rendering round- -// trips through it), no `.runner` registration is present, and its unit is neither active nor -// enabled. That tree was created by this reconcile's own ADD arm and abandoned by a width change, -// so it is Owned and teardown-eligible. Anything else -- a registered tree, an active unit, a name -// outside the grammar such as the operator scripts in the base dir -- stays OwnershipUnknown and -// refuses exactly as before. Desired members are constructed from names and carry Unobserved, -// which ownership never admits; only an observation can make a member removable. +// trips through it), its `.runner` registration is absent OR ephemeral, and its unit is neither +// active nor enabled. That tree was created by this reconcile's own ADD arm and abandoned by a +// width change, so it is Owned and teardown-eligible. Anything else -- a persistently registered +// tree, an unreadable registration, an active unit, a name outside the grammar such as the +// operator scripts in the base dir -- stays OwnershipUnknown and refuses exactly as before. +// Desired members are constructed from names and carry Unobserved, which ownership never admits; +// only an observation can make a member removable. +// +// REGISTRATION IS A KIND, NOT A BOOL. `registered: Bool` read only the file's presence, and on +// srv2 two trees (srv2-11, srv2-12) carried a `.runner` beside a unit dead since June: the file +// says `Ephemeral: True`, and an ephemeral registration is unregistered by GitHub after one job +// (extdeps.github.actions_runner actions_runner_registration_decode), so those trees were retired +// incarnations that a Bool could not distinguish from a live persistent registration. The +// persistent arm still refuses: deciding it needs an org-scoped observation this executor does +// not hold. +type RunnerSlotRegistration + = SlotUnregistered + | SlotRegisteredPersistent + | SlotRegisteredEphemeral + | SlotRegistrationUnreadable { reason: String } + type RunnerSlotProvenance = SlotProvenanceUnobserved | SlotProvenanceObserved { in_host_grammar: Bool - registered: Bool + registration: RunnerSlotRegistration unit_active: Bool unit_enabled: Bool } +fn runner_slot_registration_is_retired(r: RunnerSlotRegistration) -> Bool { + match r { + SlotUnregistered => true + SlotRegisteredEphemeral => true + SlotRegisteredPersistent => false + SlotRegistrationUnreadable { reason: _ } => false + } +} + +fn runner_slot_registration_wire(r: RunnerSlotRegistration) -> String { + match r { + SlotUnregistered => "unregistered" + SlotRegisteredPersistent => "registered" + SlotRegisteredEphemeral => "ephemeral" + SlotRegistrationUnreadable { reason: _ } => "registration-unreadable" + } +} + type RunnerSlotMember { instance_name: NonEmptyStr slot_dir: NonEmptyStr @@ -93,18 +126,18 @@ type RunnerSlotMember { fn runner_slot_provenance_is_retired_incarnation(p: RunnerSlotProvenance) -> Bool { match p { SlotProvenanceUnobserved => false - SlotProvenanceObserved { in_host_grammar: g, registered: r, unit_active: a, unit_enabled: e } => - g && !r && !a && !e + SlotProvenanceObserved { in_host_grammar: g, registration: r, unit_active: a, unit_enabled: e } => + g && runner_slot_registration_is_retired(r: r) && !a && !e } } fn runner_slot_provenance_wire(p: RunnerSlotProvenance) -> String { match p { SlotProvenanceUnobserved => "unobserved" - SlotProvenanceObserved { in_host_grammar: g, registered: r, unit_active: a, unit_enabled: e } => + SlotProvenanceObserved { in_host_grammar: g, registration: r, unit_active: a, unit_enabled: e } => join([ if g { "grammar" } else { "foreign" }, ",", - if r { "registered" } else { "unregistered" }, ",", + runner_slot_registration_wire(r: r), ",", if a { "active" } else { "inactive" }, ",", if e { "enabled" } else { "disabled" }, ], "") diff --git a/dag/gunbc/spark/secret_access_ensure.dag b/dag/gunbc/spark/secret_access_ensure.dag index d866784fc0c..c2a787960a9 100644 --- a/dag/gunbc/spark/secret_access_ensure.dag +++ b/dag/gunbc/spark/secret_access_ensure.dag @@ -126,7 +126,11 @@ fn spark_secret_grant_present(policy: GcpPolicy, member: String) -> Bool { // operation inside the arm, and return early, so the converged case performs NO WRITE. The no-op // is structural rather than promised, which is why reconcile_gcp_policy's AlreadyConverged is // consumed as its own arm rather than a delta that happens to be empty. -func spark_secret_access_ensure(token_source: AccessTokenSource) -> ProcessExit +// THE ENSURE IS OVER A TARGET SECRET, NOT OVER THE SPARK PASSWORD. The gunbai-ci App key +// (gunbc.auth.github_apps gunbai_ci_app_pem_secret) needs the same accessor binding for the same +// workload principal -- the org-actions credential is minted from it in-run and Secret Manager +// answered 403 on 2026-09-05 -- so the target is a parameter and the spark entry is one caller. +func secret_access_ensure_for(target: SecretRef, token_source: AccessTokenSource) -> ProcessExit uses net: Network { match spark_grant_member_resolution() { @@ -138,7 +142,6 @@ func spark_secret_access_ensure(token_source: AccessTokenSource) -> ProcessExit AccessTokenEnsureRefused { reason: reason } => return exit_failure(reason: reason as String) AccessTokenReady { token: token } => { - let target = spark_secret_target() observed = gcp.SecretManager.GetSecretIamPolicy( access_token: token, project_id: target.project, @@ -200,6 +203,12 @@ func spark_secret_access_ensure(token_source: AccessTokenSource) -> ProcessExit // the correct binding for a stated reason -- the control plane that may call setIamPolicy is // deliberately NOT the workload, so this entry authenticates as the human running it and never as // the convergence service account. +func spark_secret_access_ensure(token_source: AccessTokenSource) -> ProcessExit + uses net: Network +{ + secret_access_ensure_for(target: spark_secret_target(), token_source: token_source) +} + func spark_secret_access_converge() -> ProcessExit uses net: Network { diff --git a/dag/test/claim/deploy_revision_witness_test.dag b/dag/test/claim/deploy_revision_witness_test.dag index 08c27ac3672..4cd3abf4f69 100644 --- a/dag/test/claim/deploy_revision_witness_test.dag +++ b/dag/test/claim/deploy_revision_witness_test.dag @@ -7,6 +7,7 @@ import gunbc.live_deploy.revision { DeployedIsAncestor, CandidateIsAncestor, UnrelatedHistories, + DivergedHistories, RelationUnverifiable, DeployRevisionVerdict, RevisionAlreadyConverged, @@ -35,6 +36,7 @@ data all_relations: List = [ DeployedIsAncestor, CandidateIsAncestor, UnrelatedHistories, + DivergedHistories, RelationUnverifiable { cause: "probe failed" as NonEmptyStr }, ] @@ -73,6 +75,15 @@ test fn unrelated_histories_refuse() -> Bool { && !deploy_revision_terminal_success(verdict: v) } +test fn diverged_histories_refuse_with_their_own_cause() -> Bool { + match classify_deploy_revision(relation: DivergedHistories) { + RevisionRefused { cause } => + string_contains(s: cause as String, pattern: "diverged") + && !string_contains(s: cause as String, pattern: "no ancestry") + _ => false + } +} + test fn unverifiable_relation_refuses_and_keeps_its_cause() -> Bool { let why = "no persisted revision on host" as NonEmptyStr match classify_deploy_revision(relation: RelationUnverifiable { cause: why }) { @@ -113,7 +124,7 @@ test fn probe_pair_folds_to_the_four_cases() -> Bool { && match deploy_revision_relation_from_probes( deployed: sha_older, candidate: sha_newer, deployed_is_ancestor_of_candidate: false, candidate_is_ancestor_of_deployed: false - ) { UnrelatedHistories => true _ => false } + ) { RelationUnverifiable { cause: c } => string_contains(s: c as String, pattern: "merge-base") _ => false } } test fn equal_shas_short_circuit_before_the_probes() -> Bool { diff --git a/dag/test/claim/executor_privileged_operation_witness_test.dag b/dag/test/claim/executor_privileged_operation_witness_test.dag index a9c016973a1..c9ec03da6aa 100644 --- a/dag/test/claim/executor_privileged_operation_witness_test.dag +++ b/dag/test/claim/executor_privileged_operation_witness_test.dag @@ -14,6 +14,9 @@ import gunbc.generated_artifact_emit { } import gunbc.fleet_intent_network { operator_host_srv1, operator_host_srv3 } import product.placement_supply { HostIdentity } +import gunbc.runner_host_grants { runner_slot_teardown_grant_index_bound, runner_slot_grammar_dirs_to_bound, fabric_allocation_store_privileged_operations } +import gunbc.runner_slot_allocation { gunbc_runner_slots_per_host } +import gunbc.fleet_intent_network { operator_host_srv2, operator_host_srv4 } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly @@ -128,14 +131,34 @@ test fn witness_population_tracks_the_slot_roster() -> Bool { let fabric_slots = fabric_cell_host_slots(deploy: srv3_runner_host_deploy).length() let directives = fabric_cell_slice_desired_directives().length() let fabric_expected = 1 + fabric_slots * (2 + directives + 1) + let allocation_store = fabric_allocation_store_privileged_operations(deploy: srv3_runner_host_deploy).length() runner_host_privileged_operations(deploy: srv3_runner_host_deploy).length() - == (slots * 4) + 4 + fabric_expected + == runner_slot_teardown_grant_index_bound + (slots * 3) + 4 + fabric_expected + allocation_store + && allocation_store == 2 && fabric_cell_privileged_operations(deploy: srv3_runner_host_deploy).length() == fabric_expected && activation == slots * 2 && slots > 0 && fabric_slots > 0 } +// THE TEARDOWN GRANT COVERS EVERY WIDTH THE FLEET COMMITS TO, with the bound pinned above each +// host's committed width so a width change cannot outgrow its own teardown; and it is exact paths +// only -- the highest grammar dir is named and the one past the bound is not. +test fn witness_teardown_grant_bound_covers_every_committed_width_exactly() -> Bool { + let bound = runner_slot_teardown_grant_index_bound + let dirs = runner_slot_grammar_dirs_to_bound(host: operator_host_srv2, bound: bound) + bound >= gunbc_runner_slots_per_host(host: operator_host_srv1) + && bound >= gunbc_runner_slots_per_host(host: operator_host_srv2) + && bound >= gunbc_runner_slots_per_host(host: operator_host_srv3) + && bound >= gunbc_runner_slots_per_host(host: operator_host_srv4) + && bound >= 12 + && dirs.length() == bound + && any(dirs, d => (d as String) == "/opt/actions-runner/srv2-12") + && any(dirs, d => (d as String) == "/opt/actions-runner/srv2-64") + && !any(dirs, d => (d as String) == "/opt/actions-runner/srv2-65") + && !any(dirs, d => string_contains(s: d as String, pattern: "*")) +} + // THE JOIN, ASSERTED THROUGH THE PRODUCTION PATH. This is the witness the module exists for: the // command the emitted provisioning script RUNS and the command the sudoers file GRANTS are the same // word vector, reached from opposite ends of the tree. The left side comes from @@ -176,3 +199,4 @@ test fn witness_bootstrap_principal_is_not_the_grantee() -> Bool { deploy: srv3_runner_host_deploy, ) } + diff --git a/dag/test/claim/fleet/fleet_main_revision_witness_test.dag b/dag/test/claim/fleet/fleet_main_revision_witness_test.dag index 312d654aa6e..79f67211ba0 100644 --- a/dag/test/claim/fleet/fleet_main_revision_witness_test.dag +++ b/dag/test/claim/fleet/fleet_main_revision_witness_test.dag @@ -18,6 +18,7 @@ import gunbc.live_deploy.revision { DeployedIsAncestor, CandidateIsAncestor, UnrelatedHistories, + DivergedHistories, RelationUnverifiable, } import gunbc.fleet_revision_acceptance { @@ -364,7 +365,7 @@ test fn one_merge_base_decides_both_directions() -> Bool { Present { value: third } => relation_from_merge_base_oid(current: cur, accepted: acc, base: cur) == DeployedIsAncestor && relation_from_merge_base_oid(current: cur, accepted: acc, base: acc) == CandidateIsAncestor - && relation_from_merge_base_oid(current: cur, accepted: acc, base: third) == UnrelatedHistories + && relation_from_merge_base_oid(current: cur, accepted: acc, base: third) == DivergedHistories _ => false } _ => false diff --git a/dag/test/claim/fleet/fleet_revision_relation_wet_matrix_test.dag b/dag/test/claim/fleet/fleet_revision_relation_wet_matrix_test.dag index bad91be89cd..f052ae22a46 100644 --- a/dag/test/claim/fleet/fleet_revision_relation_wet_matrix_test.dag +++ b/dag/test/claim/fleet/fleet_revision_relation_wet_matrix_test.dag @@ -8,6 +8,7 @@ import gunbc.live_deploy.revision { DeployedIsAncestor, CandidateIsAncestor, UnrelatedHistories, + DivergedHistories, RelationUnverifiable, } import gunbc.fleet_main_revision { FleetRevisionRelationObservation } @@ -122,6 +123,10 @@ fn is_unrelated(r: DeployRevisionRelation?) -> Bool { match r { Present { value: UnrelatedHistories } => true _ => false } } +fn is_diverged(r: DeployRevisionRelation?) -> Bool { + match r { Present { value: DivergedHistories } => true _ => false } +} + fn is_unverifiable(r: DeployRevisionRelation?) -> Bool { match r { Present { value: RelationUnverifiable { cause: _ } } => true @@ -158,7 +163,7 @@ test fn an_ancestor_accepted_is_superseded() -> Bool { // CASE 4 -- DIVERGED, WITH A SHARED ANCESTOR. Exit 0 and a real merge base (A), which is NEITHER // endpoint. This is the case whose receipt used to claim a disjoint history. test fn siblings_sharing_an_ancestor_are_neither_ancestor_of_the_other() -> Bool { - is_unrelated(r: relation_in(repository_path: fixture_repository_path, current_hex: revision_b_hex, accepted_hex: revision_c_hex)) + is_diverged(r: relation_in(repository_path: fixture_repository_path, current_hex: revision_b_hex, accepted_hex: revision_c_hex)) } // CASE 5 -- NO COMMON ANCESTOR AT ALL. Exit 1, which is an ANSWER: git looked and there is none. diff --git a/dag/test/claim/live_deploy/target_decision_witness_test.dag b/dag/test/claim/live_deploy/target_decision_witness_test.dag index 78b69c82edc..e6ee46e2dd2 100644 --- a/dag/test/claim/live_deploy/target_decision_witness_test.dag +++ b/dag/test/claim/live_deploy/target_decision_witness_test.dag @@ -23,6 +23,7 @@ import gunbc.live_deploy.revision { DeployedIsAncestor, CandidateIsAncestor, UnrelatedHistories, + DivergedHistories, RelationUnverifiable, } import gunbc.live_deploy.target_decision { @@ -83,6 +84,7 @@ test fn witness_every_decision_lands_in_exactly_one_disposition() -> Bool { related(relation: SameRevision), related(relation: CandidateIsAncestor), related(relation: UnrelatedHistories), + related(relation: DivergedHistories), related(relation: RelationUnverifiable { cause: "probe died" as NonEmptyStr }), decide_deploy_target(input: TargetIdle { unit_state: "inactive" as NonEmptyStr }, candidate: candidate()), decide_deploy_target(input: TargetUnreadable { cause: "no answer" }, candidate: candidate()), @@ -108,8 +110,12 @@ test fn witness_a_superseded_candidate_is_a_terminal_no_op_and_never_proceeds() // UNRELATED HISTORIES AND AN UNANSWERABLE PROBE ARE THE ONLY REDS, and neither may proceed. test fn witness_unrelated_and_unverifiable_refuse_without_mutating() -> Bool { let unrelated = related(relation: UnrelatedHistories) + let diverged = related(relation: DivergedHistories) let unverifiable = related(relation: RelationUnverifiable { cause: "bad object" as NonEmptyStr }) - deploy_target_refuses(decision: unrelated) + deploy_target_refuses(decision: diverged) + && !deploy_target_proceeds(decision: diverged) + && string_contains(s: deploy_target_decision_line(decision: diverged), pattern: "diverged") + && deploy_target_refuses(decision: unrelated) && !deploy_target_proceeds(decision: unrelated) && deploy_target_refuses(decision: unverifiable) && !deploy_target_proceeds(decision: unverifiable) diff --git a/dag/test/claim/runner/runner_slot_provision_witness_test.dag b/dag/test/claim/runner/runner_slot_provision_witness_test.dag index b8586f2d092..93268f62c2a 100644 --- a/dag/test/claim/runner/runner_slot_provision_witness_test.dag +++ b/dag/test/claim/runner/runner_slot_provision_witness_test.dag @@ -4,6 +4,9 @@ import std.logic { Bool } import std.types { list_length, NonEmptyStr, List, String } import extdeps.toolchain.types { Aarch64 } import extdeps.github.actions_runner { + actions_runner_registration_decode, + ActionsRunnerRegistrationDecoded, ActionsRunnerRegistrationUnreadable, + ActionsRunnerRegistrationPersistent, ActionsRunnerRegistrationEphemeral, actions_runner_release_2_336_0, actions_runner_binary_artifact_for_arch, actions_runner_download_url, @@ -29,6 +32,8 @@ import gunbc.runner_slot_provision { RunnerSlotProvenance, SlotProvenanceObserved, SlotProvenanceUnobserved, + RunnerSlotRegistration, SlotUnregistered, SlotRegisteredPersistent, SlotRegisteredEphemeral, SlotRegistrationUnreadable, + runner_slot_provenance_wire, RunnerSlotMemberAt, desired_runner_slot_members, runner_slot_membership_reconcile, @@ -388,7 +393,7 @@ fn provenance_witness_reconcile(observed: List) -> MembershipP test fn a_retired_incarnation_is_owned_and_plans_removal() -> Bool { let retired = provenance_witness_member( name: "wfix-40", - p: SlotProvenanceObserved { in_host_grammar: true, registered: false, unit_active: false, unit_enabled: false }, + p: SlotProvenanceObserved { in_host_grammar: true, registration: SlotUnregistered, unit_active: false, unit_enabled: false }, ) (runner_slot_member_ownership(m: retired) == Present { value: Owned }) && (provenance_witness_reconcile(observed: [retired]).actions |> all(a => match a { @@ -403,9 +408,9 @@ test fn a_retired_incarnation_is_owned_and_plans_removal() -> Bool { test fn a_registered_or_active_or_enabled_tree_still_refuses_removal() -> Bool { let live = [ - provenance_witness_member(name: "wfix-41", p: SlotProvenanceObserved { in_host_grammar: true, registered: true, unit_active: false, unit_enabled: false }), - provenance_witness_member(name: "wfix-42", p: SlotProvenanceObserved { in_host_grammar: true, registered: false, unit_active: true, unit_enabled: false }), - provenance_witness_member(name: "wfix-43", p: SlotProvenanceObserved { in_host_grammar: true, registered: false, unit_active: false, unit_enabled: true }), + provenance_witness_member(name: "wfix-41", p: SlotProvenanceObserved { in_host_grammar: true, registration: SlotRegisteredPersistent, unit_active: false, unit_enabled: false }), + provenance_witness_member(name: "wfix-42", p: SlotProvenanceObserved { in_host_grammar: true, registration: SlotUnregistered, unit_active: true, unit_enabled: false }), + provenance_witness_member(name: "wfix-43", p: SlotProvenanceObserved { in_host_grammar: true, registration: SlotUnregistered, unit_active: false, unit_enabled: true }), provenance_witness_member(name: "wfix-44", p: SlotProvenanceUnobserved), ] (live |> all(m => runner_slot_member_ownership(m: m) == Absent)) @@ -419,10 +424,60 @@ test fn a_registered_or_active_or_enabled_tree_still_refuses_removal() -> Bool { })) } +// srv2-11 and srv2-12 on 2026-09-04: `.runner` present with Ephemeral=True, unit inactive and +// disabled since June. GitHub had already unregistered them (ephemeral registrations end after +// one job), so the tree is a retired incarnation; a Bool registration flag refused it forever. +test fn a_dead_ephemeral_registration_is_a_retired_incarnation() -> Bool { + let ghost = provenance_witness_member( + name: "wfix-11", + p: SlotProvenanceObserved { in_host_grammar: true, registration: SlotRegisteredEphemeral, unit_active: false, unit_enabled: false }, + ) + (runner_slot_member_ownership(m: ghost) == Present { value: Owned }) + && (provenance_witness_reconcile(observed: [ghost]).actions |> count) == 1 + && string_contains(s: runner_slot_provenance_wire(p: ghost.provenance), pattern: "ephemeral") +} + +// RED controls on the same shape: an ACTIVE ephemeral registration is a live runner, and an +// unreadable registration file decides nothing. +test fn an_active_ephemeral_registration_and_an_unreadable_one_still_refuse() -> Bool { + let live = provenance_witness_member( + name: "wfix-12", + p: SlotProvenanceObserved { in_host_grammar: true, registration: SlotRegisteredEphemeral, unit_active: true, unit_enabled: true }, + ) + let unreadable = provenance_witness_member( + name: "wfix-13", + p: SlotProvenanceObserved { in_host_grammar: true, registration: SlotRegistrationUnreadable { reason: "not json" }, unit_active: false, unit_enabled: false }, + ) + runner_slot_member_ownership(m: live) == Absent && runner_slot_member_ownership(m: unreadable) == Absent +} + +data fixture_runner_registration_ephemeral: String = "{\"AgentId\":\"1\",\"AgentName\":\"wfix-11-1-1\",\"Ephemeral\":\"True\",\"PoolId\":\"1\",\"GitHubUrl\":\"https://github.com/example\"}" +data fixture_runner_registration_persistent: String = "{\"AgentId\":\"2\",\"AgentName\":\"wfix-01\",\"PoolId\":\"1\",\"GitHubUrl\":\"https://github.com/example\"}" + +test fn the_registration_file_decodes_its_ephemeral_member() -> Bool { + let ephemeral = match actions_runner_registration_decode(raw: fixture_runner_registration_ephemeral) { + ActionsRunnerRegistrationDecoded { registration: ActionsRunnerRegistrationEphemeral } => true + _ => false + } + let persistent = match actions_runner_registration_decode(raw: fixture_runner_registration_persistent) { + ActionsRunnerRegistrationDecoded { registration: ActionsRunnerRegistrationPersistent } => true + _ => false + } + let garbage = match actions_runner_registration_decode(raw: "not json") { + ActionsRunnerRegistrationUnreadable { reason: _ } => true + _ => false + } + let widened = match actions_runner_registration_decode(raw: "{\"Ephemeral\":\"maybe\"}") { + ActionsRunnerRegistrationUnreadable { reason } => string_contains(s: reason, pattern: "outside") + _ => false + } + ephemeral && persistent && garbage && widened +} + test fn a_name_outside_the_host_grammar_refuses_even_when_dead() -> Bool { let script = provenance_witness_member( name: "jit-runner.sh", - p: SlotProvenanceObserved { in_host_grammar: false, registered: false, unit_active: false, unit_enabled: false }, + p: SlotProvenanceObserved { in_host_grammar: false, registration: SlotUnregistered, unit_active: false, unit_enabled: false }, ) runner_slot_member_ownership(m: script) == Absent } @@ -444,7 +499,7 @@ test fn the_plan_realizes_removal_through_the_privileged_seam() -> Bool { Present { value: artifact } => { let retired = provenance_witness_member( name: "wfix-40", - p: SlotProvenanceObserved { in_host_grammar: true, registered: false, unit_active: false, unit_enabled: false }, + p: SlotProvenanceObserved { in_host_grammar: true, registration: SlotUnregistered, unit_active: false, unit_enabled: false }, ) let plan = runner_slot_provision_plan(deploy: fixture_five_slot_deploy(), artifact: artifact, observed: [retired]) (plan.teardown_scripts |> count) == 1 diff --git a/dag/test/claim/workflow_dispatch_input_witness_test.dag b/dag/test/claim/workflow_dispatch_input_witness_test.dag index d773bc9c70e..87652d49ea3 100644 --- a/dag/test/claim/workflow_dispatch_input_witness_test.dag +++ b/dag/test/claim/workflow_dispatch_input_witness_test.dag @@ -154,12 +154,14 @@ test fn workflow_dispatch_choice_input_projects_options_list() -> Bool { // release binaries are produced once and consumed by everything that installs them, so a job that // mutates a host without that edge would be installing whatever happened to be lying around. test fn fleet_converge_workflow_has_build_job_needs_release_bins() -> Bool { - fleet_converge_workflow.jobs.length() == 3 + fleet_converge_workflow.jobs.length() == 4 && fleet_converge_workflow.jobs[0].id == "build" && fleet_converge_workflow.jobs[1].id == "fleet-converge" && fleet_converge_workflow.jobs[1].needs == ["build"] && fleet_converge_workflow.jobs[2].id == "dashboard-deploy" && fleet_converge_workflow.jobs[2].needs == ["build"] + && fleet_converge_workflow.jobs[3].id == "rlm-launch-deployment-receipt" + && fleet_converge_workflow.jobs[3].needs == ["build"] } fn expected_fleet_converge_yml_content() -> String { @@ -185,9 +187,17 @@ test fn fleet_converge_apply_step_binds_plan_hash_to_env_not_shell_literal() -> && string_contains(s: gunbc_ci_fleet_converge_apply_invoke(), pattern: "$\{EXPECTED_HASH:-\}") } +// The org-actions mode mints its credential in-run from the App key: the step names the App's +// installation token endpoint and the key's Secret Manager version, and the workflow carries no +// repository secret for it (the interim PAT design was superseded before it was ever taken). test fn fleet_converge_org_actions_mode_binds_secret_and_modeled_entry_holds() -> Bool { - string_contains( - s: gunbc_ci_org_actions_converge_invoke(), - pattern: "--entry dag/gunbc/fleet/org_actions_converge.dag --function org_actions_converge_wet", - ) + let invoke = gunbc_ci_org_actions_converge_invoke() + string_contains(s: invoke, pattern: "--entry dag/gunbc/fleet/org_actions_converge.dag --function org_actions_converge_wet") + && string_contains(s: invoke, pattern: "app/installations/104134109/access_tokens") + && string_contains(s: invoke, pattern: "secrets/ci-github-app-private-key/") + && string_contains(s: invoke, pattern: "openssl dgst -sha256 -sign") + && string_contains(s: invoke, pattern: "OrgAdminInstallationTokenRefused") + && string_contains(s: invoke, pattern: "OrgAdminAppKeyUnreadable") + && !string_contains(s: invoke, pattern: "curl -sSf -H @\"$HDR_FILE\" \"https://secretmanager") + && !string_contains(s: expected_fleet_converge_yml_content(), pattern: "secrets.GUNBC_ORG_ADMIN_TOKEN") } diff --git a/docs/plans/org-admin-credential-acquisition.md b/docs/plans/org-admin-credential-acquisition.md index 1de32e07cbd..46d7e818fe1 100644 --- a/docs/plans/org-admin-credential-acquisition.md +++ b/docs/plans/org-admin-credential-acquisition.md @@ -88,6 +88,13 @@ performs the live read without a code-path switch. ## Interim human handoff +**Superseded 2026-09-05, never taken.** The `org_actions_observe` step now mints a one-hour installation +token in-run from the existing `gunbai-ci` App key (Secret Manager `ci-github-app-private-key`, read +through the same WIF path as the fleet key), so no personal token is created and no Actions secret +holds a credential. The steps below are kept as the record of the design that was replaced. What +remains of the terminal migration is narrowing to a dedicated least-privilege App; the read path and +custody contract are already the terminal ones. + 1. In GitHub's fine-grained token UI, the operator selects `gunb-ai` as resource owner, grants organization `Self-hosted runners: write` and `Administration: write`, and chooses a bounded expiry. No repository content permission is needed for the org-settings probe itself. diff --git a/provisioning/srv1/gunbc-ghrunner.sudoers b/provisioning/srv1/gunbc-ghrunner.sudoers index d0b773461b1..dfe10bf696e 100644 --- a/provisioning/srv1/gunbc-ghrunner.sudoers +++ b/provisioning/srv1/gunbc-ghrunner.sudoers @@ -1,45 +1,88 @@ # GENERATED from dag/gunbc/runner/runner_host_grants.dag into provisioning/srv1/gunbc-ghrunner.sudoers — do not hand-edit; regenerate through gunbc.generated_artifact_emit artifact_generate ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-01 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-01 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-02 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-02 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-03 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-03 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-04 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-04 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-05 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-05 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-06 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-06 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-07 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-07 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-08 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-08 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-09 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-09 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-10 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-10 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-11 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-11 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-12 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-12 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-13 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-13 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-14 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-14 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-15 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-15 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-16 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-16 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-17 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-17 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-18 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-18 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-19 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-19 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-20 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-20 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-21 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-22 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-23 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-24 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-25 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-26 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-27 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-28 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-29 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-30 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-31 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-32 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-33 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-34 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-35 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-36 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-37 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-38 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-39 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-40 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-41 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-42 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-43 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-44 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-45 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-46 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-47 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-48 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-49 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-50 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-51 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-52 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-53 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-54 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-55 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-56 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-57 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-58 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-59 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-60 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-61 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-62 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-63 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv1-64 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-01 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-02 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-03 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-04 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-05 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-06 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-07 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-08 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-09 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-10 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-11 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-12 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-13 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-14 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-15 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-16 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-17 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-18 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-19 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-20 ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-21 ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv1-01.service ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv1-01.service diff --git a/provisioning/srv2/gunbc-ghrunner.sudoers b/provisioning/srv2/gunbc-ghrunner.sudoers index c729b5f24a7..fae096d343a 100644 --- a/provisioning/srv2/gunbc-ghrunner.sudoers +++ b/provisioning/srv2/gunbc-ghrunner.sudoers @@ -1,13 +1,72 @@ # GENERATED from dag/gunbc/runner/runner_host_grants.dag into provisioning/srv2/gunbc-ghrunner.sudoers — do not hand-edit; regenerate through gunbc.generated_artifact_emit artifact_generate ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-01 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv2-01 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-02 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv2-02 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-03 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv2-03 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-04 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv2-04 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-05 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-06 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-07 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-08 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-09 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-10 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-11 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-12 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-13 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-14 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-15 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-16 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-17 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-18 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-19 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-20 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-21 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-22 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-23 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-24 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-25 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-26 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-27 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-28 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-29 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-30 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-31 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-32 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-33 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-34 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-35 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-36 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-37 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-38 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-39 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-40 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-41 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-42 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-43 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-44 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-45 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-46 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-47 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-48 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-49 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-50 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-51 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-52 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-53 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-54 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-55 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-56 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-57 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-58 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-59 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-60 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-61 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-62 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-63 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv2-64 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv2-01 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv2-02 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv2-03 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv2-04 ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv2-05 ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv2-01.service ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv2-01.service diff --git a/provisioning/srv3/gunbc-ghrunner.sudoers b/provisioning/srv3/gunbc-ghrunner.sudoers index 5d349c15ed3..0058624b209 100644 --- a/provisioning/srv3/gunbc-ghrunner.sudoers +++ b/provisioning/srv3/gunbc-ghrunner.sudoers @@ -1,43 +1,87 @@ # GENERATED from dag/gunbc/runner/runner_host_grants.dag into provisioning/srv3/gunbc-ghrunner.sudoers — do not hand-edit; regenerate through gunbc.generated_artifact_emit artifact_generate ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-01 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-01 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-02 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-02 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-03 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-03 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-04 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-04 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-05 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-05 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-06 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-07 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-07 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-08 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-08 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-09 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-09 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-10 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-10 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-11 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-11 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-12 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-12 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-13 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-13 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-14 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-14 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-15 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-15 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-16 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-16 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-17 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-17 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-18 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-18 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-19 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-19 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-20 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-20 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-21 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-22 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-23 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-24 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-25 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-26 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-27 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-28 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-29 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-30 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-31 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-32 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-33 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-34 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-35 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-36 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-37 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-38 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-39 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-40 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-41 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-42 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-43 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-44 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-45 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-46 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-47 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-48 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-49 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-50 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-51 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-52 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-53 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-54 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-55 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-56 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-57 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-58 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-59 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-60 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-61 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-62 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-63 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv3-64 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-01 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-02 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-03 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-04 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-05 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-07 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-08 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-09 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-10 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-11 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-12 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-13 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-14 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-15 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-16 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-17 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-18 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-19 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-20 ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-21 ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv3-01.service ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv3-01.service diff --git a/provisioning/srv4/gunbc-ghrunner.sudoers b/provisioning/srv4/gunbc-ghrunner.sudoers index c1afb9fa20b..ade6dd4c218 100644 --- a/provisioning/srv4/gunbc-ghrunner.sudoers +++ b/provisioning/srv4/gunbc-ghrunner.sudoers @@ -1,45 +1,88 @@ # GENERATED from dag/gunbc/runner/runner_host_grants.dag into provisioning/srv4/gunbc-ghrunner.sudoers — do not hand-edit; regenerate through gunbc.generated_artifact_emit artifact_generate ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-01 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-01 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-02 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-02 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-03 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-03 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-04 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-04 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-05 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-05 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-06 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-06 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-07 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-07 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-08 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-08 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-09 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-09 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-10 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-10 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-11 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-11 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-12 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-12 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-13 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-13 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-14 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-14 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-15 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-15 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-16 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-16 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-17 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-17 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-18 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-18 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-19 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-19 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-20 -ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-20 ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-21 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-22 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-23 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-24 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-25 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-26 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-27 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-28 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-29 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-30 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-31 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-32 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-33 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-34 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-35 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-36 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-37 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-38 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-39 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-40 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-41 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-42 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-43 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-44 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-45 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-46 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-47 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-48 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-49 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-50 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-51 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-52 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-53 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-54 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-55 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-56 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-57 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-58 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-59 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-60 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-61 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-62 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-63 +ghrunner ALL=(root) NOPASSWD: /usr/bin/rm -rf /opt/actions-runner/srv4-64 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-01 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-02 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-03 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-04 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-05 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-06 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-07 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-08 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-09 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-10 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-11 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-12 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-13 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-14 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-15 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-16 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-17 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-18 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-19 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-20 ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-21 ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv4-01.service ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv4-01.service