Configure ARC Runners
1. ๐งญ Scope Legend
Use these scope markers throughout this page and the wider Kubernetes CI/CD documentation.
2. ๐งฑ Shared ARC Infrastructure Inputs
3. ๐ข Generic GitHub Organization and Repository Inputs
4. ๐งฎ Derived Organization and Repository Values
arc-systems/arc (chart 0.14.0)~/arc/<<APP_SHORT_FORM>>arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>><<APP_SHORT_FORM>>-arc-ghapp-secret<<APP_SHORT_FORM>>-ci-cd<<APP_SHORT_FORM>>-harbor-regcred<<APP_SHORT_FORM>>-harbor-credentials<<SERVICE_NAME>>-<<ENVIRONMENT>>-archttps://github.com/<<GITHUB_ORGANIZATION>>/<<SERVICE_NAME>>harbor.aspireclan.com/<<APP_SHORT_FORM>>-ci-cd/actions-runner-azcli:v1.0.0~/arc/<<APP_SHORT_FORM>>/<<SERVICE_NAME>>-<<ENVIRONMENT>>-values.yaml5. ๐ Verify Current Cluster State
Run these on ac-cicd-cp-01.
kubectl config current-contextkubectl get nodes -o widekubectl get nskubectl get crd | grep actions.github.com || truekubectl get pods -A | egrep 'arc|runner|listener' || truehelm version || truehelm list -A || true6. ๐ Verify Organization Isolation Before Changes
echo "Target GitHub organization: <<GITHUB_ORGANIZATION>>"
echo "Target app short form: <<APP_SHORT_FORM>>"
echo "Target environment: <<ENVIRONMENT>>"
echo "Target namespace: arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>>"
echo "GitHub App secret: <<APP_SHORT_FORM>>-arc-ghapp-secret"
echo "Harbor project: <<APP_SHORT_FORM>>-ci-cd"
echo "Harbor pull secret: <<APP_SHORT_FORM>>-harbor-regcred"
echo "Harbor CI credentials secret: <<APP_SHORT_FORM>>-harbor-credentials"kubectl get namespace arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>> 2>/dev/null || true
kubectl get secrets -n arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>> 2>/dev/null || true
helm list -n arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>> 2>/dev/null || true7. ๐งน Optional One-Time Cleanup of a Misplaced Repository Runner
helm list -n <<PREVIOUS_RUNNER_NAMESPACE>> | grep -F "<<SERVICE_NAME>>-<<ENVIRONMENT>>-arc" || true
kubectl get autoscalingrunnersets -n <<PREVIOUS_RUNNER_NAMESPACE>> | grep -F "<<SERVICE_NAME>>-<<ENVIRONMENT>>-arc" || truehelm uninstall <<SERVICE_NAME>>-<<ENVIRONMENT>>-arc --namespace <<PREVIOUS_RUNNER_NAMESPACE>> || truekubectl get autoscalingrunnersets -n <<PREVIOUS_RUNNER_NAMESPACE>> | grep -F "<<SERVICE_NAME>>-<<ENVIRONMENT>>-arc" || true8. ๐ท๏ธ Verify Shared Worker Labels and Taints
Run these on ac-cicd-cp-01.
kubectl get nodes --show-labelskubectl describe nodes | egrep "Name:|Roles:|Taints:|env=|workload="kubectl get nodes -L env,workloadmaxRunners across all organizations against the CPU, RAM, disk, and Docker capacity of the selected environment worker pool.9. ๐ Prepare Organization ARC Working Folder
mkdir -p ~/arc
mkdir -p ~/arc/<<APP_SHORT_FORM>>
cd ~/arc/<<APP_SHORT_FORM>>10. ๐ฆ Install Helm
sudo apt-get install curl gpg apt-transport-https --yescurl -fsSL https://packages.buildkite.com/helm-linux/helm-debian/gpgkey | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/nullecho "deb [signed-by=/usr/share/keyrings/helm.gpg] https://packages.buildkite.com/helm-linux/helm-debian/any/ any main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.listsudo apt-get update
sudo apt-get install helm
helm version11. โ๏ธ Create Shared ARC Controller Values File
cat > ~/arc/arc-controller-values.yaml <<'EOF'
replicaCount: 1
nodeSelector:
node-role.kubernetes.io/control-plane: ""
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
EOF12. ๐ Install Shared ARC Controller Only When Missing
if helm status arc -n arc-systems >/dev/null 2>&1; then
echo "Shared ARC controller arc-systems/arc already exists. Skipping installation."
helm status arc -n arc-systems
else
helm upgrade --install arc --namespace arc-systems --create-namespace --version 0.14.0 -f ~/arc/arc-controller-values.yaml oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller
fi13. โ Verify Shared ARC Controller
helm list -n arc-systemshelm status arc -n arc-systemskubectl get pods -n arc-systems -o widekubectl get deploy -n arc-systemskubectl get crd | grep actions.github.com14. ๐ Copy GitHub App PEM to the Organization Working Folder
Run this from the Windows 11 machine PowerShell.
ssh acllc@192.168.8.62 "mkdir -p ~/arc/<<APP_SHORT_FORM>>"scp "C:\Users\Manoj\Downloads\pem\<<GITHUB_APP_PEM_FILE>>" acllc@192.168.8.62:~/arc/<<APP_SHORT_FORM>>/Back on ac-cicd-cp-01:
ls -l ~/arc/<<APP_SHORT_FORM>>/*.pemchmod 600 ~/arc/<<APP_SHORT_FORM>>/*.pemhttps://github.com/organizations/<<GITHUB_ORGANIZATION>>/settings/installations/INSTALLATION_ID15. ๐งฌ Configure the Organization Runner Namespace
mkdir -p ~/arc/<<APP_SHORT_FORM>>
cd ~/arc/<<APP_SHORT_FORM>>kubectl create namespace arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>> --dry-run=client -o yaml | kubectl apply -f -kubectl get ns arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>>๐ณ Create the Organization Harbor Pull Secret
read -p "Harbor runtime robot username: " HARBOR_RUNTIME_USERNAME
read -s -p "Harbor runtime robot password: " HARBOR_RUNTIME_PASSWORD
echoCopy-pastable Harbor runtime credentials:
<<ENTER_RUNTIME_ROBOT_USERNAME>><<ENTER_RUNTIME_ROBOT_PASSWORD>>kubectl create secret docker-registry <<APP_SHORT_FORM>>-harbor-regcred --namespace arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>> --docker-server=harbor.aspireclan.com --docker-username="$HARBOR_RUNTIME_USERNAME" --docker-password="$HARBOR_RUNTIME_PASSWORD" --docker-email='noreply@aspireclan.com' --dry-run=client -o yaml | kubectl apply -f -kubectl get secret <<APP_SHORT_FORM>>-harbor-regcred -n arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>>unset HARBOR_RUNTIME_USERNAME HARBOR_RUNTIME_PASSWORDecho "USER=[$HARBOR_RUNTIME_USERNAME]"
echo "PASS=[$HARBOR_RUNTIME_PASSWORD]"16. ๐ Create the Organization GitHub App Secret
kubectl create secret generic <<APP_SHORT_FORM>>-arc-ghapp-secret --namespace arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>> --from-literal=github_app_id='<<ENTER_GITHUB_APP_ID>>' --from-literal=github_app_installation_id='<<ENTER_GITHUB_APP_INSTALLATION_ID>>' --from-file=github_app_private_key=$HOME/arc/<<APP_SHORT_FORM>>/<<GITHUB_APP_PEM_FILE>> --dry-run=client -o yaml | kubectl apply -f -kubectl get secret <<APP_SHORT_FORM>>-arc-ghapp-secret -n arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>>echo -n "App ID: "
kubectl get secret <<APP_SHORT_FORM>>-arc-ghapp-secret -n arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>> -o jsonpath='{.data.github_app_id}' | base64 -d
echo
echo -n "Installation ID: "
kubectl get secret <<APP_SHORT_FORM>>-arc-ghapp-secret -n arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>> -o jsonpath='{.data.github_app_installation_id}' | base64 -d
echo17. ๐ค Create the Organization Harbor CI Credentials Secret
read -p "Harbor CI robot username: " HARBOR_USERNAME
read -s -p "Harbor CI robot password: " HARBOR_PASSWORD
echoCopy-pastable Harbor CI credentials:
<<ENTER_CI_ROBOT_USERNAME>><<ENTER_CI_ROBOT_PASSWORD>>kubectl create secret generic <<APP_SHORT_FORM>>-harbor-credentials --namespace arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>> --from-literal=HARBOR_USERNAME="$HARBOR_USERNAME" --from-literal=HARBOR_PASSWORD="$HARBOR_PASSWORD" --dry-run=client -o yaml | kubectl apply -f -kubectl get secret <<APP_SHORT_FORM>>-harbor-credentials -n arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>>kubectl get secret <<APP_SHORT_FORM>>-harbor-credentials -n arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>> -o jsonpath='{.type}{"\n"}{.data.HARBOR_USERNAME}{"\n"}{.data.HARBOR_PASSWORD}{"\n"}'unset HARBOR_USERNAME HARBOR_PASSWORDecho "USER=[$HARBOR_USERNAME]"
echo "PASS=[$HARBOR_PASSWORD]"18. ๐ Configure GitHub Free Repository-Level Harbor Settings
read -p "Harbor CI robot username: " HARBOR_USERNAME
read -s -p "Harbor CI robot password: " HARBOR_PASSWORD
echo
REPOSITORIES=(
"<<SERVICE_NAME>>"
# "another-private-repository"
)
for REPOSITORY in "${REPOSITORIES[@]}"; do
gh secret set HARBOR_USERNAME --repo "<<GITHUB_ORGANIZATION>>/${REPOSITORY}" --body "$HARBOR_USERNAME"
gh secret set HARBOR_PASSWORD --repo "<<GITHUB_ORGANIZATION>>/${REPOSITORY}" --body "$HARBOR_PASSWORD"
gh variable set HARBOR_REGISTRY --repo "<<GITHUB_ORGANIZATION>>/${REPOSITORY}" --body "harbor.aspireclan.com"
gh variable set HARBOR_PROJECT --repo "<<GITHUB_ORGANIZATION>>/${REPOSITORY}" --body "<<APP_SHORT_FORM>>-ci-cd"
done
unset HARBOR_USERNAME HARBOR_PASSWORDREPOSITORIES=(
"<<SERVICE_NAME>>"
# "another-private-repository"
)
for REPOSITORY in "${REPOSITORIES[@]}"; do
echo "===== <<GITHUB_ORGANIZATION>>/${REPOSITORY} ====="
gh secret list --repo "<<GITHUB_ORGANIZATION>>/${REPOSITORY}"
gh variable list --repo "<<GITHUB_ORGANIZATION>>/${REPOSITORY}"
done19. ๐ Create the Repository Runner Scale Set Values File
~/arc/<<APP_SHORT_FORM>>/<<SERVICE_NAME>>-<<ENVIRONMENT>>-values.yamlcat > ~/arc/<<APP_SHORT_FORM>>/<<SERVICE_NAME>>-<<ENVIRONMENT>>-values.yaml <<'EOF'
githubConfigUrl: "https://github.com/<<GITHUB_ORGANIZATION>>/<<SERVICE_NAME>>"
githubConfigSecret: <<APP_SHORT_FORM>>-arc-ghapp-secret
runnerScaleSetName: "<<SERVICE_NAME>>-<<ENVIRONMENT>>-arc"
minRunners: 0
maxRunners: 4
listenerTemplate:
spec:
nodeSelector:
env: <<ENVIRONMENT>>
workload: arc-runner
tolerations:
- key: env
operator: Equal
value: <<ENVIRONMENT>>
effect: NoSchedule
containers:
- name: listener
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
template:
spec:
nodeSelector:
env: <<ENVIRONMENT>>
workload: arc-runner
tolerations:
- key: env
operator: Equal
value: <<ENVIRONMENT>>
effect: NoSchedule
imagePullSecrets:
- name: <<APP_SHORT_FORM>>-harbor-regcred
initContainers:
- name: init-dind-externals
image: harbor.aspireclan.com/<<APP_SHORT_FORM>>-ci-cd/actions-runner-azcli:v1.0.0
imagePullPolicy: IfNotPresent
command: ["cp", "-r", "/home/runner/externals/.", "/home/runner/tmpDir/"]
volumeMounts:
- name: dind-externals
mountPath: /home/runner/tmpDir
- name: dind
image: docker:dind
args:
- dockerd
- --host=unix:///var/run/docker.sock
- --group=$(DOCKER_GROUP_GID)
- --feature=cdi=false
env:
- name: DOCKER_GROUP_GID
value: "123"
securityContext:
privileged: true
restartPolicy: Always
startupProbe:
exec:
command: ["docker", "info"]
initialDelaySeconds: 0
failureThreshold: 24
periodSeconds: 5
volumeMounts:
- name: work
mountPath: /home/runner/_work
- name: dind-sock
mountPath: /var/run
- name: dind-externals
mountPath: /home/runner/externals
containers:
- name: runner
image: harbor.aspireclan.com/<<APP_SHORT_FORM>>-ci-cd/actions-runner-azcli:v1.0.0
imagePullPolicy: IfNotPresent
command: ["/home/runner/run.sh"]
env:
- name: DOCKER_HOST
value: unix:///var/run/docker.sock
- name: RUNNER_WAIT_FOR_DOCKER_IN_SECONDS
value: "120"
- name: HARBOR_USERNAME
valueFrom:
secretKeyRef:
name: <<APP_SHORT_FORM>>-harbor-credentials
key: HARBOR_USERNAME
- name: HARBOR_PASSWORD
valueFrom:
secretKeyRef:
name: <<APP_SHORT_FORM>>-harbor-credentials
key: HARBOR_PASSWORD
resources:
requests:
cpu: "1000m"
memory: "2Gi"
limits:
cpu: "2000m"
memory: "4Gi"
volumeMounts:
- name: work
mountPath: /home/runner/_work
- name: dind-sock
mountPath: /var/run
volumes:
- name: work
emptyDir: {}
- name: dind-sock
emptyDir: {}
- name: dind-externals
emptyDir: {}
EOF20. ๐ Review Repository Values and Organization Secret References
grep -n -E "githubConfigUrl:|githubConfigSecret:|runnerScaleSetName:|imagePullSecrets:|name: <<APP_SHORT_FORM>>-harbor-regcred|name: <<APP_SHORT_FORM>>-harbor-credentials" ~/arc/<<APP_SHORT_FORM>>/<<SERVICE_NAME>>-<<ENVIRONMENT>>-values.yamlgrep -n -A12 -B2 "listenerTemplate" ~/arc/<<APP_SHORT_FORM>>/<<SERVICE_NAME>>-<<ENVIRONMENT>>-values.yamlgrep -n -A28 -B5 "HARBOR_USERNAME" ~/arc/<<APP_SHORT_FORM>>/<<SERVICE_NAME>>-<<ENVIRONMENT>>-values.yamlgrep -n -A14 -B5 "nodeSelector" ~/arc/<<APP_SHORT_FORM>>/<<SERVICE_NAME>>-<<ENVIRONMENT>>-values.yamlgrep -n -A12 -B2 "resources:" ~/arc/<<APP_SHORT_FORM>>/<<SERVICE_NAME>>-<<ENVIRONMENT>>-values.yaml21. ๐ Install the Repository Runner Scale Set
helm upgrade --install <<SERVICE_NAME>>-<<ENVIRONMENT>>-arc --namespace arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>> --create-namespace --version 0.14.0 -f ~/arc/<<APP_SHORT_FORM>>/<<SERVICE_NAME>>-<<ENVIRONMENT>>-values.yaml --wait --timeout 5m oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set22. โ Verify Repository Runner Registration and Placement
helm status <<SERVICE_NAME>>-<<ENVIRONMENT>>-arc -n arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>>kubectl get autoscalingrunnersets -n arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>> -o widekubectl get pods -n arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>> -o widekubectl get pods -n arc-systems -o wide | grep -F "<<SERVICE_NAME>>-<<ENVIRONMENT>>-arc" || truekubectl get pods -A -o wide | egrep "arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>>|<<SERVICE_NAME>>-<<ENVIRONMENT>>-arc|listener|runner" || truekubectl get nodes -L env,workloadkubectl logs -n arc-systems deployment/arc-gha-rs-controller --all-containers=true --since=5m | grep -Ei '<<SERVICE_NAME>>|<<GITHUB_ORGANIZATION>>|error|forbidden|listener|scale set' || true23. ๐ก Watch Repository Runner Deployment
kubectl get pods -A -o wide -wRepository-specific commands:
kubectl get pods -n arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>> -wkubectl get pods -n arc-systems -w | grep --line-buffered -F "<<SERVICE_NAME>>-<<ENVIRONMENT>>-arc"24. ๐ฉบ Troubleshoot a Missing ARC Listener
minRunners: 0, no runner pod is normal until a workflow is queued, but the listener should still exist after successful registration.24.1 Confirm the failure occurs before listener creation
kubectl get autoscalingrunnersets -n arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>> -o wide
kubectl get pods -n arc-systems -o wide | grep -F "<<SERVICE_NAME>>-<<ENVIRONMENT>>-arc" || true
kubectl logs -n arc-systems deployment/arc-gha-rs-controller --all-containers=true --since=10m | grep -Ei '<<SERVICE_NAME>>|<<GITHUB_ORGANIZATION>>|registration-token|403|404|forbidden|error|listener|scale set' || trueIf the controller log shows a failed call to a repository or organization runner registration-token endpoint, continue with the probe below instead of troubleshooting node scheduling or image pulls.
24.2 Run the definitive GitHub App probe
Run this on ac-cicd-cp-01. The script reads the same Kubernetes Secret used by ARC and tests the same GitHub endpoints. It does not print the private key, installation token, or runner registration tokens.
cat > /tmp/arc-github-app-probe.sh <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail
NAMESPACE="arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>>"
SECRET_NAME="<<APP_SHORT_FORM>>-arc-ghapp-secret"
GITHUB_ORG="<<GITHUB_ORGANIZATION>>"
GITHUB_REPO="<<SERVICE_NAME>>"
API_VERSION="2022-11-28"
for command_name in kubectl jq curl openssl base64; do
if ! command -v "${command_name}" >/dev/null 2>&1; then
echo "ERROR: Required command not found: ${command_name}"
exit 1
fi
done
TMP_DIR="$(mktemp -d)"
PRIVATE_KEY_FILE="${TMP_DIR}/github-app.pem"
cleanup() {
rm -rf "${TMP_DIR}"
}
trap cleanup EXIT
decode_secret_value() {
local key="$1"
kubectl get secret "${SECRET_NAME}" --namespace "${NAMESPACE}" --output "jsonpath={.data.${key}}" |
base64 --decode
}
echo
echo "===== 1. Reading GitHub App identity from Kubernetes ====="
APP_ID="$(decode_secret_value github_app_id)"
INSTALLATION_ID="$(decode_secret_value github_app_installation_id)"
decode_secret_value github_app_private_key > "${PRIVATE_KEY_FILE}"
chmod 600 "${PRIVATE_KEY_FILE}"
echo "Namespace: ${NAMESPACE}"
echo "Secret: ${SECRET_NAME}"
echo "App ID: ${APP_ID}"
echo "Installation ID: ${INSTALLATION_ID}"
echo "Target: ${GITHUB_ORG}/${GITHUB_REPO}"
if ! openssl pkey -in "${PRIVATE_KEY_FILE}" -check -noout >/dev/null 2>&1; then
echo "ERROR: The private key stored in Kubernetes is not a valid private key."
exit 1
fi
echo "Private key: structurally valid"
base64_url_encode() {
openssl base64 -A |
tr '+/' '-_' |
tr -d '='
}
NOW="$(date +%s)"
ISSUED_AT="$((NOW - 60))"
EXPIRES_AT="$((NOW + 540))"
JWT_HEADER="$(
printf '%s' '{"alg":"RS256","typ":"JWT"}' |
base64_url_encode
)"
JWT_PAYLOAD="$(
printf '{"iat":%s,"exp":%s,"iss":"%s"}' "${ISSUED_AT}" "${EXPIRES_AT}" "${APP_ID}" |
base64_url_encode
)"
JWT_UNSIGNED="${JWT_HEADER}.${JWT_PAYLOAD}"
JWT_SIGNATURE="$(
printf '%s' "${JWT_UNSIGNED}" |
openssl dgst -sha256 -sign "${PRIVATE_KEY_FILE}" |
base64_url_encode
)"
APP_JWT="${JWT_UNSIGNED}.${JWT_SIGNATURE}"
echo
echo "===== 2. Listing installations belonging to this GitHub App ====="
INSTALLATIONS_STATUS="$(
curl --silent --show-error --output "${TMP_DIR}/installations.json" --write-out '%{http_code}' --header "Accept: application/vnd.github+json" --header "Authorization: Bearer ${APP_JWT}" --header "X-GitHub-Api-Version: ${API_VERSION}" "https://api.github.com/app/installations?per_page=100"
)"
echo "HTTP status: ${INSTALLATIONS_STATUS}"
if [[ "${INSTALLATIONS_STATUS}" == "200" ]]; then
jq --arg expected_id "${INSTALLATION_ID}" '
map({
id: (.id | tostring),
account: .account.login,
account_type: .account.type,
repository_selection: .repository_selection,
matches_kubernetes_installation_id:
((.id | tostring) == $expected_id)
})
' "${TMP_DIR}/installations.json"
else
jq '{message, documentation_url, status}' "${TMP_DIR}/installations.json" 2>/dev/null ||
cat "${TMP_DIR}/installations.json"
exit 1
fi
echo
echo "===== 3. Creating a fresh installation access token ====="
ACCESS_STATUS="$(
curl --silent --show-error --request POST --dump-header "${TMP_DIR}/access-token.headers" --output "${TMP_DIR}/access-token.json" --write-out '%{http_code}' --header "Accept: application/vnd.github+json" --header "Authorization: Bearer ${APP_JWT}" --header "X-GitHub-Api-Version: ${API_VERSION}" "https://api.github.com/app/installations/${INSTALLATION_ID}/access_tokens"
)"
echo "HTTP status: ${ACCESS_STATUS}"
if [[ "${ACCESS_STATUS}" != "201" ]]; then
jq 'del(.token)' "${TMP_DIR}/access-token.json" 2>/dev/null ||
cat "${TMP_DIR}/access-token.json"
exit 1
fi
jq '{
expires_at,
repository_selection,
permissions
} | del(.token)' "${TMP_DIR}/access-token.json"
INSTALLATION_TOKEN="$(
jq --raw-output '.token // empty' "${TMP_DIR}/access-token.json"
)"
if [[ -z "${INSTALLATION_TOKEN}" ]]; then
echo "ERROR: GitHub returned no installation token."
exit 1
fi
echo
echo "===== 4. Repositories visible to the installation token ====="
VISIBLE_REPOSITORIES_STATUS="$(
curl --silent --show-error --output "${TMP_DIR}/repositories.json" --write-out '%{http_code}' --header "Accept: application/vnd.github+json" --header "Authorization: Bearer ${INSTALLATION_TOKEN}" --header "X-GitHub-Api-Version: ${API_VERSION}" "https://api.github.com/installation/repositories?per_page=100"
)"
echo "HTTP status: ${VISIBLE_REPOSITORIES_STATUS}"
if [[ "${VISIBLE_REPOSITORIES_STATUS}" == "200" ]]; then
jq --raw-output '.repositories[]?.full_name' "${TMP_DIR}/repositories.json"
if jq --exit-status --arg target "${GITHUB_ORG}/${GITHUB_REPO}" '
any(
.repositories[]?;
((.full_name | ascii_downcase) ==
($target | ascii_downcase))
)
' "${TMP_DIR}/repositories.json" >/dev/null; then
echo
echo "TARGET_REPOSITORY_VISIBLE=yes"
else
echo
echo "TARGET_REPOSITORY_VISIBLE=no"
fi
else
jq '{message, documentation_url, status}' "${TMP_DIR}/repositories.json" 2>/dev/null ||
cat "${TMP_DIR}/repositories.json"
fi
echo
echo "===== 5. Testing normal repository visibility ====="
REPOSITORY_GET_STATUS="$(
curl --silent --show-error --output "${TMP_DIR}/repository-get.json" --write-out '%{http_code}' --header "Accept: application/vnd.github+json" --header "Authorization: Bearer ${INSTALLATION_TOKEN}" --header "X-GitHub-Api-Version: ${API_VERSION}" "https://api.github.com/repos/${GITHUB_ORG}/${GITHUB_REPO}"
)"
echo "REPOSITORY_GET_HTTP=${REPOSITORY_GET_STATUS}"
if [[ "${REPOSITORY_GET_STATUS}" == "200" ]]; then
jq '{
full_name,
private,
archived,
disabled,
permissions
}' "${TMP_DIR}/repository-get.json"
else
jq '{message, documentation_url, status}' "${TMP_DIR}/repository-get.json" 2>/dev/null ||
cat "${TMP_DIR}/repository-get.json"
fi
echo
echo "===== 6. Testing the exact repository registration call used by ARC ====="
REPOSITORY_REGISTRATION_STATUS="$(
curl --silent --show-error --request POST --dump-header "${TMP_DIR}/repository-registration.headers" --output "${TMP_DIR}/repository-registration.json" --write-out '%{http_code}' --header "Accept: application/vnd.github+json" --header "Authorization: Bearer ${INSTALLATION_TOKEN}" --header "X-GitHub-Api-Version: ${API_VERSION}" "https://api.github.com/repos/${GITHUB_ORG}/${GITHUB_REPO}/actions/runners/registration-token"
)"
echo "REPOSITORY_REGISTRATION_HTTP=${REPOSITORY_REGISTRATION_STATUS}"
grep --ignore-case '^x-accepted-github-permissions:' "${TMP_DIR}/repository-registration.headers" || true
if [[ "${REPOSITORY_REGISTRATION_STATUS}" == "201" ]]; then
echo "Repository registration token call succeeded."
echo "The returned temporary token is intentionally not displayed."
else
jq '{message, documentation_url, status}' "${TMP_DIR}/repository-registration.json" 2>/dev/null ||
cat "${TMP_DIR}/repository-registration.json"
fi
echo
echo "===== 7. Testing organization-level registration ====="
ORGANIZATION_REGISTRATION_STATUS="$(
curl --silent --show-error --request POST --dump-header "${TMP_DIR}/organization-registration.headers" --output "${TMP_DIR}/organization-registration.json" --write-out '%{http_code}' --header "Accept: application/vnd.github+json" --header "Authorization: Bearer ${INSTALLATION_TOKEN}" --header "X-GitHub-Api-Version: ${API_VERSION}" "https://api.github.com/orgs/${GITHUB_ORG}/actions/runners/registration-token"
)"
echo "ORGANIZATION_REGISTRATION_HTTP=${ORGANIZATION_REGISTRATION_STATUS}"
grep --ignore-case '^x-accepted-github-permissions:' "${TMP_DIR}/organization-registration.headers" || true
if [[ "${ORGANIZATION_REGISTRATION_STATUS}" == "201" ]]; then
echo "Organization registration token call succeeded."
echo "The returned temporary token is intentionally not displayed."
else
jq '{message, documentation_url, status}' "${TMP_DIR}/organization-registration.json" 2>/dev/null ||
cat "${TMP_DIR}/organization-registration.json"
fi
echo
echo "===== RESULT SUMMARY ====="
echo "Installation token: ${ACCESS_STATUS}"
echo "Repository visible: ${REPOSITORY_GET_STATUS}"
echo "Repository registration: ${REPOSITORY_REGISTRATION_STATUS}"
echo "Organization registration:${ORGANIZATION_REGISTRATION_STATUS}"
EOF
chmod 700 /tmp/arc-github-app-probe.sh
/tmp/arc-github-app-probe.sh24.3 Interpret the four summary results
Installation token:
Repository visible:
Repository registration:
Organization registration:Result A โ Target repository is not visible
TARGET_REPOSITORY_VISIBLE=no
REPOSITORY_GET_HTTP=404The GitHub App installation represented by github_app_installation_id cannot see the configured repository. Verify the exact organization login and repository name, confirm that the matching Installation ID belongs to that organization, and add the repository under the installed appโs selected repository access.
kubectl get secret <<APP_SHORT_FORM>>-arc-ghapp-secret -n arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>> -o jsonpath='{.data.github_app_installation_id}' | base64 -d
echo
echo "Expected target: <<GITHUB_ORGANIZATION>>/<<SERVICE_NAME>>"Result B โ Repository is visible, but registration returns 403
REPOSITORY_GET_HTTP=200
REPOSITORY_REGISTRATION_HTTP=403
Resource not accessible by integrationThe fresh installation token must include these permissions:
- Repository Administration โ Read and write
- Repository Metadata โ Read-only
- Repository Contents โ Read-only
- Organization Self-hosted runners โ Read and write
When permissions were added after the app was installed, an organization owner must approve them under:
<<GITHUB_ORGANIZATION>> โ Settings โ GitHub Apps โ Installed GitHub Apps โ Configure
โ Review request / Approve new permissions / Accept new permissionsRe-run the probe after approval. It creates a fresh installation token, so missing permissions in that result cannot be explained by an old cached token.
Result C โ Repository GET succeeds, repository registration is 404, organization registration is 201
REPOSITORY_GET_HTTP=200
REPOSITORY_REGISTRATION_HTTP=404
ORGANIZATION_REGISTRATION_HTTP=201This can indicate a repository-level GitHub/ARC limitation involving an enterprise-created GitHub App installed into an organization. The durable options are to create the GitHub App directly under the target organization or use organization-level ARC registration with appropriate runner-group access controls.
# Repository-level registration
githubConfigUrl: "https://github.com/<<GITHUB_ORGANIZATION>>/<<SERVICE_NAME>>"
# Organization-level alternative
githubConfigUrl: "https://github.com/<<GITHUB_ORGANIZATION>>"Result D โ Repository registration succeeds with 201
REPOSITORY_REGISTRATION_HTTP=201GitHub authentication is correct. Force ARC to obtain fresh credentials and reconcile the existing scale set:
kubectl rollout restart deployment/arc-gha-rs-controller --namespace arc-systems
kubectl rollout status deployment/arc-gha-rs-controller --namespace arc-systems --timeout=2m
kubectl annotate autoscalingrunnerset <<SERVICE_NAME>>-<<ENVIRONMENT>>-arc --namespace arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>> troubleshooting.arc/reconcile-at="$(date +%s)" --overwrite24.4 Watch the fresh reconciliation without filtering the first error
Start with the unfiltered controller log because a narrow grep can hide the first meaningful error after successful GitHub authentication.
kubectl logs --namespace arc-systems deployment/arc-gha-rs-controller --all-containers=true --since=3m --followIn another terminal:
kubectl get autoscalingrunnersets,autoscalinglisteners,ephemeralrunnersets,ephemeralrunners,pods --all-namespaces --watch24.5 Inspect the live resource rather than only the local values file
helm get values <<SERVICE_NAME>>-<<ENVIRONMENT>>-arc -n arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>>
kubectl get autoscalingrunnerset <<SERVICE_NAME>>-<<ENVIRONMENT>>-arc --namespace arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>> --output yaml201, the controller assigns a runner scale-set ID, and a listener named from <<SERVICE_NAME>>-<<ENVIRONMENT>>-arc appears in arc-systems on the worker selected for <<ENVIRONMENT>>.