Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -76,7 +79,7 @@ bower_components
build/Release

# Dependency directories
/node_modules
node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)
Expand Down Expand Up @@ -126,6 +129,7 @@ out

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
Expand Down Expand Up @@ -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
4 changes: 2 additions & 2 deletions action.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: "Render Deploy Action - fork"
name: "Render Deploy Action"
description: "Trigger a Render service deploy"
inputs:
service-id:
Expand All @@ -11,5 +11,5 @@ inputs:
description: "Should job wait for deployment to succeed"
required: false
runs:
using: "node16"
using: "node24"
main: "dist/index.js"
88 changes: 72 additions & 16 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8740,24 +8740,63 @@ 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 {
throw new Error(
`Render API returned non-JSON response (HTTP ${response.status}): ${text.slice(0, 200)}`,
);
}
}

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}` },
},
`https://api.render.com/v1/services/${SERVICEID}/deploys/${deployId}`,
{ headers: RENDER_HEADERS },
);

const data = await response.json();
if (response.ok) {
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)}`,
);
}
}

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);
Expand All @@ -8766,7 +8805,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;
}

Expand All @@ -8784,31 +8823,48 @@ 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}` },
headers: RENDER_HEADERS,
},
);

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;
}

core.info(`Deploy ${data.status} - Commit: ${data.commit.message}`);
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) {
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);
}
}

Expand Down
88 changes: 72 additions & 16 deletions src/action.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,63 @@ 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 {
throw new Error(
`Render API returned non-JSON response (HTTP ${response.status}): ${text.slice(0, 200)}`,
);
}
}

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}` },
},
`https://api.render.com/v1/services/${SERVICEID}/deploys/${deployId}`,
{ headers: RENDER_HEADERS },
);

const data = await response.json();
if (response.ok) {
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)}`,
);
}
}

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);
Expand All @@ -32,7 +71,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;
}

Expand All @@ -50,31 +89,48 @@ 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}` },
headers: RENDER_HEADERS,
},
);

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;
}

core.info(`Deploy ${data.status} - Commit: ${data.commit.message}`);
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) {
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);
}
}

Expand Down