From fa6b04a5c99c0754f13f2f6488e2271c3547d34c Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Tue, 20 Oct 2026 06:55:13 +0000 Subject: [PATCH 1/3] feat(coder/github-upload-public-key): add key_type to upload SSH signing keys Add an optional key_type variable (auth, signing, both) so the Coder public key can also be uploaded as a GitHub SSH signing key. The default stays auth to preserve the current behavior. Supersedes #1072. --- .../github-upload-public-key/README.md | 26 ++- .../github-upload-public-key/main.test.ts | 167 +++++++++++------- .../modules/github-upload-public-key/main.tf | 11 ++ .../github-upload-public-key/main.tftest.hcl | 51 ++++++ .../modules/github-upload-public-key/run.sh | 102 ++++++----- 5 files changed, 248 insertions(+), 109 deletions(-) create mode 100644 registry/coder/modules/github-upload-public-key/main.tftest.hcl diff --git a/registry/coder/modules/github-upload-public-key/README.md b/registry/coder/modules/github-upload-public-key/README.md index 0f8fc52c7..817d99416 100644 --- a/registry/coder/modules/github-upload-public-key/README.md +++ b/registry/coder/modules/github-upload-public-key/README.md @@ -14,7 +14,7 @@ Templates that utilize Github External Auth can automatically ensure that the Co module "github-upload-public-key" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/github-upload-public-key/coder" - version = "1.1.0" + version = "1.2.0" agent_id = coder_agent.main.id key_name = "ACME Coder Workspaces" } @@ -22,6 +22,22 @@ module "github-upload-public-key" { `key_name` is optional. If omitted, GitHub displays the uploaded key as ` Workspaces`. +## Commit signing + +By default, the module uploads the Coder public key as a GitHub authentication key. Set `key_type` to `signing` to upload it as a GitHub SSH signing key instead, so GitHub can verify commits signed with it, or to `both` to upload it as both: + +```tf +module "github-upload-public-key" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/github-upload-public-key/coder" + version = "1.2.0" + agent_id = coder_agent.main.id + key_type = "both" +} +``` + +This module only uploads the key. Git inside the workspace must still be configured to sign commits with the Coder SSH key. + ## Requirements This module requires `curl` and `jq` to be installed inside your workspace. @@ -38,6 +54,12 @@ CODER_EXTERNAL_AUTH_0_SCOPES="repo,workflow,admin:public_key" Note that the default scopes if not provided are `repo,workflow`. If the module is failing to complete after updating the external auth configuration, instruct users of the module to "Unlink" and "Link" their Github account in the External Auth user settings page to get the new scopes. +When `key_type` is `signing` or `both`, the Github app must also have read and write permissions to "SSH signing keys", and the external auth configuration must include the `admin:ssh_signing_key` scope. For example: + +```txt +CODER_EXTERNAL_AUTH_0_SCOPES="repo,workflow,admin:public_key,admin:ssh_signing_key" +``` + ## Example Using a coder github external auth with a non-default id: (default is `github`) @@ -50,7 +72,7 @@ data "coder_external_auth" "github" { module "github-upload-public-key" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/github-upload-public-key/coder" - version = "1.1.0" + version = "1.2.0" agent_id = coder_agent.main.id external_auth_id = data.coder_external_auth.github.id } diff --git a/registry/coder/modules/github-upload-public-key/main.test.ts b/registry/coder/modules/github-upload-public-key/main.test.ts index 7e7f2b8fa..692ec6989 100644 --- a/registry/coder/modules/github-upload-public-key/main.test.ts +++ b/registry/coder/modules/github-upload-public-key/main.test.ts @@ -35,11 +35,16 @@ afterEach(async () => { } }); +type Upload = { path: string; title: string }; + +const AUTH_KEYS_PATH = "/user/keys"; +const SIGNING_KEYS_PATH = "/user/ssh_signing_keys"; + const setupContainer = async ( image = "lorello/alpine-bash", vars: Record = {}, ) => { - const { server, uploadedKeyNames } = setupServer(); + const { server, uploads } = setupServer(); const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", ...vars, @@ -54,11 +59,24 @@ const setupContainer = async ( await removeContainer(id); }); - return { id, instance, server, uploadedKeyNames }; + const url = server.url.toString().slice(0, -1); + const run = () => + execContainer(id, [ + "env", + `CODER_ACCESS_URL=${url}`, + `GITHUB_API_URL=${url}`, + "CODER_OWNER_SESSION_TOKEN=foo", + "CODER_EXTERNAL_AUTH_ID=github", + "bash", + "-c", + instance.script, + ]); + + return { id, url, run, uploads }; }; const setupServer = () => { - const uploadedKeyNames: string[] = []; + const uploads: Upload[] = []; const server = serve({ fetch: async (req) => { const url = new URL(req.url); @@ -68,10 +86,13 @@ const setupServer = () => { }); } - if (url.pathname === "/user/keys") { + if ( + url.pathname === AUTH_KEYS_PATH || + url.pathname === SIGNING_KEYS_PATH + ) { if (req.method === "POST") { const body = (await req.json()) as { title: string }; - uploadedKeyNames.push(body.title); + uploads.push({ path: url.pathname, title: body.title }); return createJSONResponse( { key: "created", @@ -110,7 +131,7 @@ const setupServer = () => { port: 0, }); - return { server, uploadedKeyNames }; + return { server, uploads }; }; setDefaultTimeout(30 * 1000); @@ -125,98 +146,112 @@ describe("github-upload-public-key", () => { }); it("creates new key if one does not exist", async () => { - const { instance, id, server, uploadedKeyNames } = await setupContainer(); + const { id, url, run, uploads } = await setupContainer(); await writeCoder(id, "echo foo"); - const url = server.url.toString().slice(0, -1); - const exec = await execContainer(id, [ - "env", - `CODER_ACCESS_URL=${url}`, - `GITHUB_API_URL=${url}`, - "CODER_OWNER_SESSION_TOKEN=foo", - "CODER_EXTERNAL_AUTH_ID=github", - "bash", - "-c", - instance.script, - ]); + const exec = await run(); expect(exec.stdout).toContain( - "Your Coder public key has been added to GitHub!", + "Your Coder public key has been added as a GitHub authentication key!", ); expect(exec.exitCode).toBe(0); - expect(uploadedKeyNames).toEqual([`${url} Workspaces`]); + expect(uploads).toEqual([ + { path: AUTH_KEYS_PATH, title: `${url} Workspaces` }, + ]); }); it("uses a custom key name", async () => { const keyName = "ACME Coder Workspaces"; - const { instance, id, server, uploadedKeyNames } = await setupContainer( - undefined, - { key_name: keyName }, - ); + const { id, run, uploads } = await setupContainer(undefined, { + key_name: keyName, + }); await writeCoder(id, "echo foo"); - const url = server.url.toString().slice(0, -1); - const exec = await execContainer(id, [ - "env", - `CODER_ACCESS_URL=${url}`, - `GITHUB_API_URL=${url}`, - "CODER_OWNER_SESSION_TOKEN=foo", - "CODER_EXTERNAL_AUTH_ID=github", - "bash", - "-c", - instance.script, - ]); + const exec = await run(); expect(exec.stdout).toContain( - "Your Coder public key has been added to GitHub!", + "Your Coder public key has been added as a GitHub authentication key!", ); expect(exec.exitCode).toBe(0); - expect(uploadedKeyNames).toEqual([keyName]); + expect(uploads).toEqual([{ path: AUTH_KEYS_PATH, title: keyName }]); }); it("uses a key name with special characters", async () => { const keyName = 'Special $(id) "chars" `test`'; - const { instance, id, server, uploadedKeyNames } = await setupContainer( - undefined, - { key_name: keyName }, - ); + const { id, run, uploads } = await setupContainer(undefined, { + key_name: keyName, + }); await writeCoder(id, "echo foo"); - const url = server.url.toString().slice(0, -1); - const exec = await execContainer(id, [ - "env", - `CODER_ACCESS_URL=${url}`, - `GITHUB_API_URL=${url}`, - "CODER_OWNER_SESSION_TOKEN=foo", - "CODER_EXTERNAL_AUTH_ID=github", - "bash", - "-c", - instance.script, - ]); + const exec = await run(); expect(exec.stdout).toContain( - "Your Coder public key has been added to GitHub!", + "Your Coder public key has been added as a GitHub authentication key!", ); expect(exec.exitCode).toBe(0); - expect(uploadedKeyNames).toEqual([keyName]); + expect(uploads).toEqual([{ path: AUTH_KEYS_PATH, title: keyName }]); }); it("does nothing if one already exists", async () => { - const { instance, id, server } = await setupContainer(); + const { id, run, uploads } = await setupContainer(); // use keyword to make server return a existing key await writeCoder(id, "echo findkey"); - const url = server.url.toString().slice(0, -1); - const exec = await execContainer(id, [ - "env", - `CODER_ACCESS_URL=${url}`, - `GITHUB_API_URL=${url}`, - "CODER_OWNER_SESSION_TOKEN=foo", - "CODER_EXTERNAL_AUTH_ID=github", - "bash", - "-c", - instance.script, + const exec = await run(); + expect(exec.stdout).toContain( + "Your Coder public key is already a GitHub authentication key!", + ); + expect(exec.exitCode).toBe(0); + expect(uploads).toEqual([]); + }); + + it("uploads only a signing key when key_type is signing", async () => { + const { id, url, run, uploads } = await setupContainer(undefined, { + key_type: "signing", + }); + await writeCoder(id, "echo foo"); + + const exec = await run(); + expect(exec.stdout).toContain( + "Your Coder public key has been added as a GitHub signing key!", + ); + expect(exec.exitCode).toBe(0); + expect(uploads).toEqual([ + { path: SIGNING_KEYS_PATH, title: `${url} Workspaces` }, ]); + }); + + it("uploads authentication and signing keys when key_type is both", async () => { + const { id, url, run, uploads } = await setupContainer(undefined, { + key_type: "both", + }); + await writeCoder(id, "echo foo"); + + const exec = await run(); + expect(exec.stdout).toContain( + "Your Coder public key has been added as a GitHub authentication key!", + ); + expect(exec.stdout).toContain( + "Your Coder public key has been added as a GitHub signing key!", + ); + expect(exec.exitCode).toBe(0); + expect(uploads).toEqual([ + { path: AUTH_KEYS_PATH, title: `${url} Workspaces` }, + { path: SIGNING_KEYS_PATH, title: `${url} Workspaces` }, + ]); + }); + + it("skips existing keys when key_type is both", async () => { + const { id, run, uploads } = await setupContainer(undefined, { + key_type: "both", + }); + await writeCoder(id, "echo findkey"); + + const exec = await run(); + expect(exec.stdout).toContain( + "Your Coder public key is already a GitHub authentication key!", + ); expect(exec.stdout).toContain( - "Your Coder public key is already on GitHub!", + "Your Coder public key is already a GitHub signing key!", ); expect(exec.exitCode).toBe(0); + expect(uploads).toEqual([]); }); }); diff --git a/registry/coder/modules/github-upload-public-key/main.tf b/registry/coder/modules/github-upload-public-key/main.tf index 48da51ab9..cf87f0b65 100644 --- a/registry/coder/modules/github-upload-public-key/main.tf +++ b/registry/coder/modules/github-upload-public-key/main.tf @@ -32,6 +32,16 @@ variable "key_name" { default = null } +variable "key_type" { + description = "Where to upload the Coder public key on GitHub: 'auth' for an authentication key, 'signing' for a commit signing key, or 'both'." + type = string + default = "auth" + validation { + condition = contains(["auth", "signing", "both"], var.key_type) + error_message = "key_type must be one of 'auth', 'signing', or 'both'." + } +} + data "coder_workspace" "me" {} data "coder_workspace_owner" "me" {} @@ -43,6 +53,7 @@ resource "coder_script" "github_upload_public_key" { CODER_EXTERNAL_AUTH_ID : var.external_auth_id, GITHUB_API_URL : var.github_api_url, CODER_PUBLIC_KEY_NAME : base64encode(var.key_name == null ? "" : var.key_name), + KEY_TYPE : var.key_type, }) display_name = "Github Upload Public Key" icon = "/icon/github.svg" diff --git a/registry/coder/modules/github-upload-public-key/main.tftest.hcl b/registry/coder/modules/github-upload-public-key/main.tftest.hcl new file mode 100644 index 000000000..fb11a13cc --- /dev/null +++ b/registry/coder/modules/github-upload-public-key/main.tftest.hcl @@ -0,0 +1,51 @@ +run "defaults_to_auth_key_type" { + command = plan + + variables { + agent_id = "test-agent-id" + } + + assert { + condition = strcontains(coder_script.github_upload_public_key.script, "KEY_TYPE='auth'") + error_message = "key_type should default to auth" + } +} + +run "passes_signing_key_type" { + command = plan + + variables { + agent_id = "test-agent-id" + key_type = "signing" + } + + assert { + condition = strcontains(coder_script.github_upload_public_key.script, "KEY_TYPE='signing'") + error_message = "key_type signing should be passed to the script" + } +} + +run "passes_both_key_type" { + command = plan + + variables { + agent_id = "test-agent-id" + key_type = "both" + } + + assert { + condition = strcontains(coder_script.github_upload_public_key.script, "KEY_TYPE='both'") + error_message = "key_type both should be passed to the script" + } +} + +run "rejects_invalid_key_type" { + command = plan + + variables { + agent_id = "test-agent-id" + key_type = "invalid" + } + + expect_failures = [var.key_type] +} diff --git a/registry/coder/modules/github-upload-public-key/run.sh b/registry/coder/modules/github-upload-public-key/run.sh index c80efb869..1813895d6 100644 --- a/registry/coder/modules/github-upload-public-key/run.sh +++ b/registry/coder/modules/github-upload-public-key/run.sh @@ -61,51 +61,71 @@ if [ -z "$PUBLIC_KEY" ]; then exit 1 fi -echo "Fetching public keys from GitHub..." -GITHUB_KEYS_RESPONSE=$( - curl -L -s \ - -w "\n%%{http_code}" \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer $GITHUB_TOKEN" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - $GITHUB_API_URL/user/keys -) -GITHUB_KEYS_RESPONSE_STATUS=$(tail -n1 <<< "$GITHUB_KEYS_RESPONSE") -GITHUB_KEYS_RESPONSE_BODY=$(sed \$d <<< "$GITHUB_KEYS_RESPONSE") +CODER_PUBLIC_KEY_NAME="$(printf '%s' '${CODER_PUBLIC_KEY_NAME}' | base64 -d)" +[ -n "$CODER_PUBLIC_KEY_NAME" ] || CODER_PUBLIC_KEY_NAME="$CODER_ACCESS_URL Workspaces" -if [ "$GITHUB_KEYS_RESPONSE_STATUS" -ne 200 ]; then - echo "Failed to fetch Coder public SSH key with status code $GITHUB_KEYS_RESPONSE_STATUS!" - echo "$GITHUB_KEYS_RESPONSE_BODY" - exit 1 -fi +upload_key() { + local endpoint="$1" + local label="$2" -GITHUB_MATCH=$(jq -r --arg PUBLIC_KEY "$PUBLIC_KEY" '.[] | select(.key == $PUBLIC_KEY) | .key' <<< "$GITHUB_KEYS_RESPONSE_BODY") + echo "Fetching $label keys from GitHub..." + local keys_response + keys_response=$( + curl -L -s \ + -w "\n%%{http_code}" \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "$GITHUB_API_URL/user/$endpoint" + ) + local keys_status keys_body + keys_status=$(tail -n1 <<< "$keys_response") + keys_body=$(sed \$d <<< "$keys_response") -if [ "$PUBLIC_KEY" = "$GITHUB_MATCH" ]; then - echo "Your Coder public key is already on GitHub!" - exit 0 -fi + if [ "$keys_status" -ne 200 ]; then + echo "Failed to fetch GitHub $label keys with status code $keys_status!" + echo "$keys_body" + return 1 + fi -echo "Your Coder public key is not in GitHub. Adding it now..." -CODER_PUBLIC_KEY_NAME="$(printf '%s' '${CODER_PUBLIC_KEY_NAME}' | base64 -d)" -[ -n "$CODER_PUBLIC_KEY_NAME" ] || CODER_PUBLIC_KEY_NAME="$CODER_ACCESS_URL Workspaces" -UPLOAD_RESPONSE=$( - curl -L -s \ - -X POST \ - -w "\n%%{http_code}" \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer $GITHUB_TOKEN" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - $GITHUB_API_URL/user/keys \ - -d "$(jq -nc --arg title "$CODER_PUBLIC_KEY_NAME" --arg key "$PUBLIC_KEY" '{title: $title, key: $key}')" -) -UPLOAD_RESPONSE_STATUS=$(tail -n1 <<< "$UPLOAD_RESPONSE") -UPLOAD_RESPONSE_BODY=$(sed \$d <<< "$UPLOAD_RESPONSE") + local match + match=$(jq -r --arg PUBLIC_KEY "$PUBLIC_KEY" '.[] | select(.key == $PUBLIC_KEY) | .key' <<< "$keys_body") + if [ "$PUBLIC_KEY" = "$match" ]; then + echo "Your Coder public key is already a GitHub $label key!" + return 0 + fi -if [ "$UPLOAD_RESPONSE_STATUS" -ne 201 ]; then - echo "Failed to upload Coder public SSH key with status code $UPLOAD_RESPONSE_STATUS!" - echo "$UPLOAD_RESPONSE_BODY" - exit 1 + echo "Your Coder public key is not a GitHub $label key yet. Adding it now..." + local upload_response + upload_response=$( + curl -L -s \ + -X POST \ + -w "\n%%{http_code}" \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "$GITHUB_API_URL/user/$endpoint" \ + -d "$(jq -nc --arg title "$CODER_PUBLIC_KEY_NAME" --arg key "$PUBLIC_KEY" '{title: $title, key: $key}')" + ) + local upload_status upload_body + upload_status=$(tail -n1 <<< "$upload_response") + upload_body=$(sed \$d <<< "$upload_response") + + if [ "$upload_status" -ne 201 ]; then + echo "Failed to upload Coder public SSH key as a GitHub $label key with status code $upload_status!" + echo "$upload_body" + return 1 + fi + + echo "Your Coder public key has been added as a GitHub $label key!" +} + +KEY_TYPE='${KEY_TYPE}' + +if [ "$KEY_TYPE" = "auth" ] || [ "$KEY_TYPE" = "both" ]; then + upload_key "keys" "authentication" || exit 1 fi -echo "Your Coder public key has been added to GitHub!" +if [ "$KEY_TYPE" = "signing" ] || [ "$KEY_TYPE" = "both" ]; then + upload_key "ssh_signing_keys" "signing" || exit 1 +fi From 8daec43065003490081bb128adebc8af091baae8 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Tue, 20 Oct 2026 16:08:39 +0000 Subject: [PATCH 2/3] docs(coder/github-upload-public-key): recommend write:ssh_signing_key scope --- registry/coder/modules/github-upload-public-key/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/registry/coder/modules/github-upload-public-key/README.md b/registry/coder/modules/github-upload-public-key/README.md index 817d99416..88ef06f06 100644 --- a/registry/coder/modules/github-upload-public-key/README.md +++ b/registry/coder/modules/github-upload-public-key/README.md @@ -54,10 +54,10 @@ CODER_EXTERNAL_AUTH_0_SCOPES="repo,workflow,admin:public_key" Note that the default scopes if not provided are `repo,workflow`. If the module is failing to complete after updating the external auth configuration, instruct users of the module to "Unlink" and "Link" their Github account in the External Auth user settings page to get the new scopes. -When `key_type` is `signing` or `both`, the Github app must also have read and write permissions to "SSH signing keys", and the external auth configuration must include the `admin:ssh_signing_key` scope. For example: +When `key_type` is `signing` or `both`, the Github app must also have read and write permissions to "SSH signing keys", and the external auth configuration must include the `write:ssh_signing_key` scope. For example: ```txt -CODER_EXTERNAL_AUTH_0_SCOPES="repo,workflow,admin:public_key,admin:ssh_signing_key" +CODER_EXTERNAL_AUTH_0_SCOPES="repo,workflow,admin:public_key,write:ssh_signing_key" ``` ## Example From 779ce3152249ad5db65de71a008f623264e9ada2 Mon Sep 17 00:00:00 2001 From: Jay Date: Tue, 20 Oct 2026 23:42:58 +0530 Subject: [PATCH 3/3] fix(coder/github-upload-public-key): follow pagination when checking existing keys (#1197) ## Description Stacked on #1193. Addresses [this review comment](https://github.com/coder/registry/pull/1193#discussion_r4326467489). GitHub's key list endpoints return 30 keys per page by default, so a Coder key beyond the first page was missed and the module tried to upload it again. `upload_key` now requests `per_page=100` and keeps fetching pages until it finds the key or gets a page shorter than 100. This applies to both `/user/keys` and `/user/ssh_signing_keys`. ## Type of Change - [ ] New module - [ ] New template - [x] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/github-upload-public-key` **New version:** `v1.2.0` (unreleased; same version as #1193) **Breaking change:** [ ] Yes [x] No ## Testing & Validation - [x] Tests pass (`bun test`): 10/10, with a new case where the key is on the second page for both endpoints. This case fails on the #1193 script. - [x] Code formatted (`bun fmt`) - [x] Changes tested locally against real GitHub ## Related Issues Stacked on #1193 --- > [!NOTE] > This PR was generated by Coder Agents on behalf of @35C4n0r, using Claude. --- .../github-upload-public-key/main.test.ts | 30 ++++++++++ .../modules/github-upload-public-key/run.sh | 57 +++++++++++-------- 2 files changed, 62 insertions(+), 25 deletions(-) diff --git a/registry/coder/modules/github-upload-public-key/main.test.ts b/registry/coder/modules/github-upload-public-key/main.test.ts index 692ec6989..c5193b440 100644 --- a/registry/coder/modules/github-upload-public-key/main.test.ts +++ b/registry/coder/modules/github-upload-public-key/main.test.ts @@ -113,6 +113,19 @@ const setupServer = () => { ]); } + // case: key exists on the second page + if (req.headers.get("Authorization") === "Bearer paginated") { + const perPage = Number(url.searchParams.get("per_page") ?? 30); + const page = Number(url.searchParams.get("page") ?? 1); + const keys = [ + ...Array.from({ length: perPage }, (_, i) => ({ key: `foo-${i}` })), + { key: "exists" }, + ]; + return createJSONResponse( + keys.slice((page - 1) * perPage, page * perPage), + ); + } + // case: key does not exist return createJSONResponse([ { @@ -238,6 +251,23 @@ describe("github-upload-public-key", () => { ]); }); + it("finds an existing key on a later page", async () => { + const { id, run, uploads } = await setupContainer(undefined, { + key_type: "both", + }); + await writeCoder(id, "echo paginated"); + + const exec = await run(); + expect(exec.stdout).toContain( + "Your Coder public key is already a GitHub authentication key!", + ); + expect(exec.stdout).toContain( + "Your Coder public key is already a GitHub signing key!", + ); + expect(exec.exitCode).toBe(0); + expect(uploads).toEqual([]); + }); + it("skips existing keys when key_type is both", async () => { const { id, run, uploads } = await setupContainer(undefined, { key_type: "both", diff --git a/registry/coder/modules/github-upload-public-key/run.sh b/registry/coder/modules/github-upload-public-key/run.sh index 1813895d6..58f816d49 100644 --- a/registry/coder/modules/github-upload-public-key/run.sh +++ b/registry/coder/modules/github-upload-public-key/run.sh @@ -69,31 +69,38 @@ upload_key() { local label="$2" echo "Fetching $label keys from GitHub..." - local keys_response - keys_response=$( - curl -L -s \ - -w "\n%%{http_code}" \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer $GITHUB_TOKEN" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "$GITHUB_API_URL/user/$endpoint" - ) - local keys_status keys_body - keys_status=$(tail -n1 <<< "$keys_response") - keys_body=$(sed \$d <<< "$keys_response") - - if [ "$keys_status" -ne 200 ]; then - echo "Failed to fetch GitHub $label keys with status code $keys_status!" - echo "$keys_body" - return 1 - fi - - local match - match=$(jq -r --arg PUBLIC_KEY "$PUBLIC_KEY" '.[] | select(.key == $PUBLIC_KEY) | .key' <<< "$keys_body") - if [ "$PUBLIC_KEY" = "$match" ]; then - echo "Your Coder public key is already a GitHub $label key!" - return 0 - fi + local per_page=100 page=1 + local keys_response keys_status keys_body match count + while true; do + keys_response=$( + curl -L -s \ + -w "\n%%{http_code}" \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "$GITHUB_API_URL/user/$endpoint?per_page=$per_page&page=$page" + ) + keys_status=$(tail -n1 <<< "$keys_response") + keys_body=$(sed \$d <<< "$keys_response") + + if [ "$keys_status" -ne 200 ]; then + echo "Failed to fetch GitHub $label keys with status code $keys_status!" + echo "$keys_body" + return 1 + fi + + match=$(jq -r --arg PUBLIC_KEY "$PUBLIC_KEY" '.[] | select(.key == $PUBLIC_KEY) | .key' <<< "$keys_body") + if [ "$PUBLIC_KEY" = "$match" ]; then + echo "Your Coder public key is already a GitHub $label key!" + return 0 + fi + + count=$(jq 'length' <<< "$keys_body") + if [ "$count" -lt "$per_page" ]; then + break + fi + page=$((page + 1)) + done echo "Your Coder public key is not a GitHub $label key yet. Adding it now..." local upload_response