From a2133450cce6ef8a548bd618708fc444d0efa5f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20C=C3=A9sar=C3=A9-Herriau?= Date: Wed, 22 Oct 2025 12:49:18 -0400 Subject: [PATCH 1/3] Upgrade Node to 24, add logs --- .gitignore | 12 ++++++++---- action.yml | 4 ++-- dist/index.js | 25 +++++++++++++++++-------- package-lock.json | 2 ++ src/action.js | 25 +++++++++++++++++-------- 5 files changed, 46 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index bdeae76..03bd3b5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ -# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,node -# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,macos,node +# Created by https://www.toptal.com/developers/gitignore/api/node,macos,visualstudiocode,asdf +# Edit at https://www.toptal.com/developers/gitignore?templates=node,macos,visualstudiocode,asdf + +### asdf ### +/.tool-versions ### macOS ### # General @@ -76,7 +79,7 @@ bower_components build/Release # Dependency directories -/node_modules +node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) @@ -126,6 +129,7 @@ out # Nuxt.js build / generate output .nuxt +dist # Gatsby files .cache/ @@ -192,4 +196,4 @@ out .history .ionide -# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,node +# End of https://www.toptal.com/developers/gitignore/api/node,macos,visualstudiocode,asdf \ No newline at end of file diff --git a/action.yml b/action.yml index 6e93b55..8019f74 100644 --- a/action.yml +++ b/action.yml @@ -1,4 +1,4 @@ -name: "Render Deploy Action - fork" +name: "Render Deploy Action" description: "Trigger a Render service deploy" inputs: service-id: @@ -11,5 +11,5 @@ inputs: description: "Should job wait for deployment to succeed" required: false runs: - using: "node16" + using: "node24" main: "dist/index.js" diff --git a/dist/index.js b/dist/index.js index 8f07c9f..b918e98 100644 --- a/dist/index.js +++ b/dist/index.js @@ -8742,22 +8742,24 @@ const WAIT_FOR_SUCCESS = async function retrieveStatus(deployId) { const response = await fetch( - "https://api.render.com/v1/services/" + SERVICEID + "/deploys/" + deployId, + `https://api.render.com/v1/services/${SERVICEID}/deploys/${deployId}`, { headers: { Authorization: `Bearer ${APIKEY}` }, }, ); - const data = await response.json(); if (response.ok) { + const data = await response.json(); return data.status; } else { throw Error("Could not retrieve deploy information.") } } -async function waitForSuccess(data) { - let previousStatus = ""; +async function waitForSuccess(data, currentStatus) { + core.info(`Waiting for deploy to succeed`); + + let previousStatus = currentStatus; while (true) { await new Promise((res) => { setTimeout(res, 10000); @@ -8766,7 +8768,7 @@ async function waitForSuccess(data) { const status = await retrieveStatus(data.id); if (status !== previousStatus) { - core.info(`Deploy status: ${status}`); + core.info(`Deploy status changed: ${status}`); previousStatus = status; } @@ -8784,7 +8786,7 @@ async function waitForSuccess(data) { async function run() { const response = await fetch( - "https://api.render.com/v1/services/" + SERVICEID + "/deploys", + `https://api.render.com/v1/services/${SERVICEID}/deploys`, { method: "POST", headers: { Authorization: `Bearer ${APIKEY}` }, @@ -8805,10 +8807,17 @@ async function run() { return; } - core.info(`Deploy ${data.status} - Commit: ${data.commit.message}`); + let ref = "unknown"; + if (data.commit) { + ref = `git commit: ${data.commit.message}`; + } else if (data.image) { + ref = `image: ${data.image.ref} SHA: ${data.image.sha}`; + } + core.info(`Deploy triggered for ${ref}`); + core.info(`Status: ${data.status}`); if (WAIT_FOR_SUCCESS) { - await waitForSuccess(data); + await waitForSuccess(data, data.status); } } diff --git a/package-lock.json b/package-lock.json index 37c11b7..8a50359 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,6 +59,7 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.5.1.tgz", "integrity": "sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw==", "dev": true, + "peer": true, "dependencies": { "@octokit/auth-token": "^2.4.4", "@octokit/graphql": "^4.5.8", @@ -394,6 +395,7 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.5.1.tgz", "integrity": "sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw==", "dev": true, + "peer": true, "requires": { "@octokit/auth-token": "^2.4.4", "@octokit/graphql": "^4.5.8", diff --git a/src/action.js b/src/action.js index fcbad36..68f8a00 100644 --- a/src/action.js +++ b/src/action.js @@ -8,22 +8,24 @@ const WAIT_FOR_SUCCESS = async function retrieveStatus(deployId) { const response = await fetch( - "https://api.render.com/v1/services/" + SERVICEID + "/deploys/" + deployId, + `https://api.render.com/v1/services/${SERVICEID}/deploys/${deployId}`, { headers: { Authorization: `Bearer ${APIKEY}` }, }, ); - const data = await response.json(); if (response.ok) { + const data = await response.json(); return data.status; } else { throw Error("Could not retrieve deploy information.") } } -async function waitForSuccess(data) { - let previousStatus = ""; +async function waitForSuccess(data, currentStatus) { + core.info(`Waiting for deploy to succeed`); + + let previousStatus = currentStatus; while (true) { await new Promise((res) => { setTimeout(res, 10000); @@ -32,7 +34,7 @@ async function waitForSuccess(data) { const status = await retrieveStatus(data.id); if (status !== previousStatus) { - core.info(`Deploy status: ${status}`); + core.info(`Deploy status changed: ${status}`); previousStatus = status; } @@ -50,7 +52,7 @@ async function waitForSuccess(data) { async function run() { const response = await fetch( - "https://api.render.com/v1/services/" + SERVICEID + "/deploys", + `https://api.render.com/v1/services/${SERVICEID}/deploys`, { method: "POST", headers: { Authorization: `Bearer ${APIKEY}` }, @@ -71,10 +73,17 @@ async function run() { return; } - core.info(`Deploy ${data.status} - Commit: ${data.commit.message}`); + let ref = "unknown"; + if (data.commit) { + ref = `git commit: ${data.commit.message}`; + } else if (data.image) { + ref = `image: ${data.image.ref} SHA: ${data.image.sha}`; + } + core.info(`Deploy triggered for ${ref}`); + core.info(`Status: ${data.status}`); if (WAIT_FOR_SUCCESS) { - await waitForSuccess(data); + await waitForSuccess(data, data.status); } } From d0b12af2b41c2fafb123113244374e13f10da070 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20C=C3=A9sar=C3=A9-Herriau?= Date: Fri, 20 Feb 2026 13:54:26 -0800 Subject: [PATCH 2/3] fix: handle non-JSON responses from Render API The action crashes with "Unexpected end of JSON input" when the Render API returns an empty body or non-JSON response (e.g. 429 rate limit, 502 gateway error). This is a flaky CI failure that requires manual re-runs. Changes: - Add parseJsonResponse() helper that reads text first, then parses - Move response.json() call after HTTP status checks in run() - Include HTTP status and response body snippet in all error messages Co-Authored-By: Claude Opus 4.6 --- dist/index.js | 25 ++++++++++++++++++++----- src/action.js | 25 ++++++++++++++++++++----- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/dist/index.js b/dist/index.js index b918e98..11c8d27 100644 --- a/dist/index.js +++ b/dist/index.js @@ -8740,6 +8740,17 @@ const APIKEY = core.getInput("api-key") || process.env.APIKEY; const WAIT_FOR_SUCCESS = core.getInput("wait-for-success") || process.env.WAIT_FOR_SUCCESS; +async function parseJsonResponse(response) { + const text = await response.text(); + try { + return JSON.parse(text); + } catch { + throw new Error( + `Render API returned non-JSON response (HTTP ${response.status}): ${text.slice(0, 200)}`, + ); + } +} + async function retrieveStatus(deployId) { const response = await fetch( `https://api.render.com/v1/services/${SERVICEID}/deploys/${deployId}`, @@ -8749,10 +8760,13 @@ async function retrieveStatus(deployId) { ); if (response.ok) { - const data = await response.json(); + const data = await parseJsonResponse(response); return data.status; } else { - throw Error("Could not retrieve deploy information.") + const text = await response.text(); + throw new Error( + `Could not retrieve deploy information (HTTP ${response.status}): ${text.slice(0, 200)}`, + ); } } @@ -8793,20 +8807,21 @@ async function run() { }, ); - const data = await response.json(); - if (response.status === 401) { core.setFailed( "Render Deploy Action: Unauthorized. Please check your API key.", ); return; } else if (!response.ok) { + const text = await response.text(); core.setFailed( - `Deploy error: ${data.message} (status code ${response.status})`, + `Deploy error (HTTP ${response.status}): ${text.slice(0, 200)}`, ); return; } + const data = await parseJsonResponse(response); + let ref = "unknown"; if (data.commit) { ref = `git commit: ${data.commit.message}`; diff --git a/src/action.js b/src/action.js index 68f8a00..d3e44cc 100644 --- a/src/action.js +++ b/src/action.js @@ -6,6 +6,17 @@ const APIKEY = core.getInput("api-key") || process.env.APIKEY; const WAIT_FOR_SUCCESS = core.getInput("wait-for-success") || process.env.WAIT_FOR_SUCCESS; +async function parseJsonResponse(response) { + const text = await response.text(); + try { + return JSON.parse(text); + } catch { + throw new Error( + `Render API returned non-JSON response (HTTP ${response.status}): ${text.slice(0, 200)}`, + ); + } +} + async function retrieveStatus(deployId) { const response = await fetch( `https://api.render.com/v1/services/${SERVICEID}/deploys/${deployId}`, @@ -15,10 +26,13 @@ async function retrieveStatus(deployId) { ); if (response.ok) { - const data = await response.json(); + const data = await parseJsonResponse(response); return data.status; } else { - throw Error("Could not retrieve deploy information.") + const text = await response.text(); + throw new Error( + `Could not retrieve deploy information (HTTP ${response.status}): ${text.slice(0, 200)}`, + ); } } @@ -59,20 +73,21 @@ async function run() { }, ); - const data = await response.json(); - if (response.status === 401) { core.setFailed( "Render Deploy Action: Unauthorized. Please check your API key.", ); return; } else if (!response.ok) { + const text = await response.text(); core.setFailed( - `Deploy error: ${data.message} (status code ${response.status})`, + `Deploy error (HTTP ${response.status}): ${text.slice(0, 200)}`, ); return; } + const data = await parseJsonResponse(response); + let ref = "unknown"; if (data.commit) { ref = `git commit: ${data.commit.message}`; From 6e56f932e4722ef1d6b4a5e7cf72286858510241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20C=C3=A9sar=C3=A9-Herriau?= Date: Fri, 20 Feb 2026 14:20:01 -0800 Subject: [PATCH 3/3] fix: handle HTTP 202 responses with empty body The Render API can return 202 Accepted with an empty body when a deploy is queued. Previously this threw a non-JSON parse error. Now we detect the empty body and poll the deploys list to fetch the latest deploy. Co-Authored-By: Claude Opus 4.6 --- dist/index.js | 42 +++++++++++++++++++++++++++++++++++++----- package-lock.json | 2 -- src/action.js | 42 +++++++++++++++++++++++++++++++++++++----- 3 files changed, 74 insertions(+), 12 deletions(-) diff --git a/dist/index.js b/dist/index.js index 11c8d27..8ee88e1 100644 --- a/dist/index.js +++ b/dist/index.js @@ -8740,8 +8740,13 @@ const APIKEY = core.getInput("api-key") || process.env.APIKEY; const WAIT_FOR_SUCCESS = core.getInput("wait-for-success") || process.env.WAIT_FOR_SUCCESS; +const RENDER_HEADERS = { Authorization: `Bearer ${APIKEY}` }; + async function parseJsonResponse(response) { const text = await response.text(); + if (!text.trim()) { + return null; + } try { return JSON.parse(text); } catch { @@ -8751,12 +8756,30 @@ async function parseJsonResponse(response) { } } +async function fetchLatestDeploy() { + const response = await fetch( + `https://api.render.com/v1/services/${SERVICEID}/deploys?limit=1`, + { headers: RENDER_HEADERS }, + ); + + if (!response.ok) { + const text = await response.text(); + throw new Error( + `Could not list deploys (HTTP ${response.status}): ${text.slice(0, 200)}`, + ); + } + + const deploys = await parseJsonResponse(response); + if (!deploys || deploys.length === 0) { + throw new Error("No deploys found after triggering deploy"); + } + return deploys[0]; +} + async function retrieveStatus(deployId) { const response = await fetch( `https://api.render.com/v1/services/${SERVICEID}/deploys/${deployId}`, - { - headers: { Authorization: `Bearer ${APIKEY}` }, - }, + { headers: RENDER_HEADERS }, ); if (response.ok) { @@ -8803,7 +8826,7 @@ async function run() { `https://api.render.com/v1/services/${SERVICEID}/deploys`, { method: "POST", - headers: { Authorization: `Bearer ${APIKEY}` }, + headers: RENDER_HEADERS, }, ); @@ -8820,7 +8843,16 @@ async function run() { return; } - const data = await parseJsonResponse(response); + let data = await parseJsonResponse(response); + + // HTTP 202 (Accepted) may return an empty body — the deploy was queued but + // the response doesn't include deploy details. Poll the deploys list instead. + if (!data) { + core.info( + `Deploy accepted (HTTP ${response.status}) with empty body, fetching latest deploy...`, + ); + data = await fetchLatestDeploy(); + } let ref = "unknown"; if (data.commit) { diff --git a/package-lock.json b/package-lock.json index 8a50359..37c11b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,7 +59,6 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.5.1.tgz", "integrity": "sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw==", "dev": true, - "peer": true, "dependencies": { "@octokit/auth-token": "^2.4.4", "@octokit/graphql": "^4.5.8", @@ -395,7 +394,6 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.5.1.tgz", "integrity": "sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw==", "dev": true, - "peer": true, "requires": { "@octokit/auth-token": "^2.4.4", "@octokit/graphql": "^4.5.8", diff --git a/src/action.js b/src/action.js index d3e44cc..24566e5 100644 --- a/src/action.js +++ b/src/action.js @@ -6,8 +6,13 @@ const APIKEY = core.getInput("api-key") || process.env.APIKEY; const WAIT_FOR_SUCCESS = core.getInput("wait-for-success") || process.env.WAIT_FOR_SUCCESS; +const RENDER_HEADERS = { Authorization: `Bearer ${APIKEY}` }; + async function parseJsonResponse(response) { const text = await response.text(); + if (!text.trim()) { + return null; + } try { return JSON.parse(text); } catch { @@ -17,12 +22,30 @@ async function parseJsonResponse(response) { } } +async function fetchLatestDeploy() { + const response = await fetch( + `https://api.render.com/v1/services/${SERVICEID}/deploys?limit=1`, + { headers: RENDER_HEADERS }, + ); + + if (!response.ok) { + const text = await response.text(); + throw new Error( + `Could not list deploys (HTTP ${response.status}): ${text.slice(0, 200)}`, + ); + } + + const deploys = await parseJsonResponse(response); + if (!deploys || deploys.length === 0) { + throw new Error("No deploys found after triggering deploy"); + } + return deploys[0]; +} + async function retrieveStatus(deployId) { const response = await fetch( `https://api.render.com/v1/services/${SERVICEID}/deploys/${deployId}`, - { - headers: { Authorization: `Bearer ${APIKEY}` }, - }, + { headers: RENDER_HEADERS }, ); if (response.ok) { @@ -69,7 +92,7 @@ async function run() { `https://api.render.com/v1/services/${SERVICEID}/deploys`, { method: "POST", - headers: { Authorization: `Bearer ${APIKEY}` }, + headers: RENDER_HEADERS, }, ); @@ -86,7 +109,16 @@ async function run() { return; } - const data = await parseJsonResponse(response); + let data = await parseJsonResponse(response); + + // HTTP 202 (Accepted) may return an empty body — the deploy was queued but + // the response doesn't include deploy details. Poll the deploys list instead. + if (!data) { + core.info( + `Deploy accepted (HTTP ${response.status}) with empty body, fetching latest deploy...`, + ); + data = await fetchLatestDeploy(); + } let ref = "unknown"; if (data.commit) {