Skip to content

Commit

Permalink
Handle GitHub private repositories (#236)
Browse files Browse the repository at this point in the history
  • Loading branch information
Vadorequest committed Dec 30, 2020
1 parent 2d1aa53 commit 38713b4
Show file tree
Hide file tree
Showing 9 changed files with 202 additions and 118 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/deploy-vercel-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ jobs:
timeout: 90 # Wait for 90 seconds before failing

- name: Display deployment status
run: "echo My deployment is ${{ fromJson(steps.await-vercel.outputs.deploymentDetails).readyState }}"
run: "echo The deployment is ${{ fromJson(steps.await-vercel.outputs.deploymentDetails).readyState }}"

# Send a HTTP call to the webhook url that's provided in the customer configuration file (vercel.*.json)
send-webhook-callback-once-deployment-ready:
Expand All @@ -194,7 +194,8 @@ jobs:
- name: Expose git environment variables and call webhook (if provided)
# Workflow overview:
# - Resolves webhook url from customer config file
# - If a webhook url was defined, send a
# - If a webhook url was defined in the customer config file, send an HTTP request, as POST request, with a JSON request body dynamically generated
# - Prints the headers of the POST HTTP request (curl)
run: |
MANUAL_TRIGGER_CUSTOMER="${{ github.event.inputs.customer}}"
CUSTOMER_REF_TO_DEPLOY="${MANUAL_TRIGGER_CUSTOMER:-$(cat vercel.json | jq --raw-output '.build.env.NEXT_PUBLIC_CUSTOMER_REF')}"
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/deploy-vercel-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ jobs:
timeout: 90 # Wait for 90 seconds before failing

- name: Display deployment status
run: "echo My deployment is ${{ fromJson(steps.await-vercel.outputs.deploymentDetails).readyState }}"
run: "echo The deployment is ${{ fromJson(steps.await-vercel.outputs.deploymentDetails).readyState }}"

# Send a HTTP call to the webhook url that's provided in the customer configuration file (vercel.*.json)
send-webhook-callback-once-deployment-ready:
Expand All @@ -271,7 +271,8 @@ jobs:
- name: Expose git environment variables and call webhook (if provided)
# Workflow overview:
# - Resolves webhook url from customer config file
# - If a webhook url was defined, send a
# - If a webhook url was defined in the customer config file, send an HTTP request, as POST request, with a JSON request body dynamically generated
# - Prints the headers of the POST HTTP request (curl)
run: |
MANUAL_TRIGGER_CUSTOMER="${{ github.event.inputs.customer}}"
CUSTOMER_REF_TO_DEPLOY="${MANUAL_TRIGGER_CUSTOMER:-$(cat vercel.json | jq --raw-output '.build.env.NEXT_PUBLIC_CUSTOMER_REF')}"
Expand Down
4 changes: 2 additions & 2 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ console.debug(`Building Next with NODE_ENV="${process.env.NODE_ENV}" NEXT_PUBLIC
const GIT_COMMIT_TAGS = process.env.GIT_COMMIT_TAGS ? process.env.GIT_COMMIT_TAGS.trim() : '';
console.debug(`Deployment will be tagged automatically, using GIT_COMMIT_TAGS: "${GIT_COMMIT_TAGS}"`);

// Iterate over all tags and extract the first the match "v*" and extract only the version number ("v${major}.${minor}.${patch})
const APP_RELEASE_TAG = GIT_COMMIT_TAGS ? GIT_COMMIT_TAGS.split(' ').find((tag) => tag.startsWith('v')).split('-')[0] : `unknown-${GIT_COMMIT_SHA_SHORT}`;
// Iterate over all tags and extract the first the match "v*"
const APP_RELEASE_TAG = GIT_COMMIT_TAGS ? GIT_COMMIT_TAGS.split(' ').find((tag) => tag.startsWith('v')) : `unknown-${GIT_COMMIT_SHA_SHORT}`;
console.debug(`Release version resolved from tags: "${APP_RELEASE_TAG}" (matching first tag starting with "v")`);

/**
Expand Down
1 change: 1 addition & 0 deletions src/pages/api/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const status = async (req: NextApiRequest, res: NextApiResponse): Promise
appName: process.env.NEXT_PUBLIC_APP_NAME,
appRelease: process.env.NEXT_PUBLIC_APP_VERSION_RELEASE,
appBuildTime: process.env.NEXT_PUBLIC_APP_BUILD_TIME,
appBuildTimeISO: (new Date(process.env.NEXT_PUBLIC_APP_BUILD_TIME)).toISOString(),
appBuildTimestamp: process.env.NEXT_PUBLIC_APP_BUILD_TIMESTAMP,
appBuildId: process.env.NEXT_PUBLIC_APP_BUILD_ID,
nodejs: process.version,
Expand Down
118 changes: 60 additions & 58 deletions src/pages/api/webhooks/deploymentCompleted.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,64 +16,66 @@ const logger = createLogger({
label: fileLabel,
});

type EndpointRequestBody = {
/**
* Value that was typed in the GHA web page, as "Customer to deploy".
*
* If this value is defined, it means the deployment was manually triggered through the Github Actions page (by a human being).
*
* @example customer1
* @see https://github.com/UnlyEd/next-right-now/actions
*/
MANUAL_TRIGGER_CUSTOMER: string;

/**
* Ref of the customer that was actually deployed.
*
* Computed value at runtime (during CI), depending on MANUAL_TRIGGER_CUSTOMER.
* Fallback to the default customer (defined in the vercel.json file).
*
* @example customer1
*/
CUSTOMER_REF?: string;

/**
* Stage (production|staging) used for the deployment.
*
* @example production
* @example staging
*/
STAGE?: string;

/**
* SHA of the git commit used as deployment ref.
*
* Resolved by GitHub Action automatically, depending on which GIT_COMMIT_REF was used.
*
* @example 23ad5f7cc9a4b6c35e9c8d796fea13dcb3a08238
*/
GIT_COMMIT_SHA?: string;

/**
* Ref (branch|tag) that was used as deployment ref.
*
* When the deployment was performed manually, it corresponds to the ref selected in the "Use workflow from" list (UI).
* When the deployment was performed automatically (git push), it corresponds to the branch that was being used.
*
* @example refs/heads/master
*/
GIT_COMMIT_REF?: string;

/**
* All tags associated with the git commit.
*
* Will contain the version number that was automatically assigned with the commit.
*
* @example v4.0.34-custom-webhooks-in-ci
*/
GIT_COMMIT_TAGS?: string;
};

type EndpointRequest = NextApiRequest & {
body: {
/**
* Value that was typed in the GHA web page, as "Customer to deploy".
*
* If this value is defined, it means the deployment was manually triggered through the Github Actions page (by a human being).
*
* @example customer1
* @see https://github.com/UnlyEd/next-right-now/actions
*/
MANUAL_TRIGGER_CUSTOMER: string;

/**
* Ref of the customer that was actually deployed.
*
* Computed value at runtime (during CI), depending on MANUAL_TRIGGER_CUSTOMER.
* Fallback to the default customer (defined in the vercel.json file).
*
* @example customer1
*/
CUSTOMER_REF?: string;

/**
* Stage (production|staging) used for the deployment.
*
* @example production
* @example staging
*/
STAGE?: string;

/**
* SHA of the git commit used as deployment ref.
*
* Resolved by GitHub Action automatically, depending on which GIT_COMMIT_REF was used.
*
* @example 23ad5f7cc9a4b6c35e9c8d796fea13dcb3a08238
*/
GIT_COMMIT_SHA?: string;

/**
* Ref (branch|tag) that was used as deployment ref.
*
* When the deployment was performed manually, it corresponds to the ref selected in the "Use workflow from" list (UI).
* When the deployment was performed automatically (git push), it corresponds to the branch that was being used.
*
* @example refs/heads/master
*/
GIT_COMMIT_REF?: string;

/**
* All tags associated with the git commit.
*
* Will contain the version number that was automatically assigned with the commit.
*
* @example v4.0.34-custom-webhooks-in-ci
*/
GIT_COMMIT_TAGS?: string;
}
body: EndpointRequestBody;
};

/**
Expand All @@ -97,7 +99,7 @@ export const deploymentCompleted = async (req: EndpointRequest, res: NextApiResp
// eslint-disable-next-line no-console
console.log(req?.body);

const parsedBody: GenericObject = convertRequestBodyToJSObject(req);
const parsedBody = convertRequestBodyToJSObject(req) as EndpointRequestBody;

// eslint-disable-next-line no-console
console.debug('body (parsed)', parsedBody);
Expand Down
2 changes: 1 addition & 1 deletion src/utils/api/convertRequestBodyToJSObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { GenericObject } from '../../types/GenericObject';
export const convertRequestBodyToJSObject = <T = unknown>(req: NextApiRequest): GenericObject<T> => {
let parsedBody: GenericObject<T> = {};

if (typeof req?.body === 'string') {
if (typeof req?.body === 'string' && req?.body?.length > 0) {
parsedBody = JSON.parse(req?.body);
} else {
parsedBody = req.body;
Expand Down
131 changes: 84 additions & 47 deletions src/utils/gitHubActions/dispatchWorkflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,65 +17,102 @@ const logger = createLogger({
* @param workflowFilePath
*/
export const dispatchWorkflow = async (workflowsList: WorkflowsAPIResponse, platformReleaseRef: string, workflowFilePath: string): Promise<void> => {
const [workflowDetails] = workflowsList?.workflows?.filter((workflow) => workflow?.path === workflowFilePath);
try {
const [workflowDetails] = workflowsList?.workflows?.filter((workflow) => workflow?.path === workflowFilePath);

if (workflowDetails) {
const body = {
inputs: {
customer: process.env.NEXT_PUBLIC_CUSTOMER_REF,
},
ref: platformReleaseRef,
};
const options = {
method: 'POST',
headers: {
Authorization: `token ${process.env.GITHUB_DISPATCH_TOKEN}`,
Accept: 'application/vnd.github.v3+json',
},
body: JSON.stringify(body),
};
const url = `${workflowDetails?.url}/dispatches`;
if (workflowDetails) {
/**
* Creates a workflow dispatch event.
*
* @see https://docs.github.com/en/free-pro-team@latest/rest/reference/actions#create-a-workflow-dispatch-event
*/
const url = `${workflowDetails?.url}/dispatches`;
const body = {
inputs: {
customer: process.env.NEXT_PUBLIC_CUSTOMER_REF,
},
ref: platformReleaseRef,
};
const options = {
method: 'POST',
headers: {
Accept: 'application/vnd.github.v3+json',
},
body: JSON.stringify(body),
};

Sentry.configureScope((scope): void => {
scope.setExtra('workflowFilePath', workflowFilePath);
scope.setExtra('workflowDispatchRequestUrl', url);
scope.setContext('workflowDispatchRequestBody', body);
scope.setContext('workflowDetails', workflowDetails);
});
if (process.env.GITHUB_DISPATCH_TOKEN) {
// Authorization token, required if the repository is private, unnecessary if the repo is public
options.headers['Authorization'] = `token ${process.env.GITHUB_DISPATCH_TOKEN}`;
}

Sentry.configureScope((scope): void => {
scope.setExtra('workflowFilePath', workflowFilePath);
scope.setExtra('workflowDispatchRequestUrl', url);
scope.setExtra('platformReleaseRef', platformReleaseRef);
scope.setContext('workflowDispatchRequestBody', body);
scope.setContext('workflowDetails', workflowDetails);
});

Sentry.withScope((scope): void => {
scope.setTag('alertType', ALERT_TYPES.VERCEL_DEPLOYMENT_TRIGGERED);
Sentry.withScope((scope): void => {
scope.setTag('alertType', ALERT_TYPES.VERCEL_DEPLOYMENT_TRIGGER_ATTEMPT);

Sentry.captureEvent({
message: 'Triggering Vercel deployment.',
level: Sentry.Severity.Log,
Sentry.captureEvent({
message: `Attempting to trigger a Vercel deployment using "${workflowFilePath}" with version "${platformReleaseRef}".`,
level: Sentry.Severity.Log,
});
});
});

logger.debug(`Fetching "${url}", using workflow path: "${workflowFilePath}", with request body: ${JSON.stringify(body, null, 2)}`);
const response = await fetch(url, options);
logger.debug(`Fetching "${url}", using workflow path: "${workflowFilePath}", with request body: ${JSON.stringify(body, null, 2)}`);
const response = await fetch(url, options);

if (!response?.status.toString().startsWith('2')) {
// If the response status isn't 2XX, then something wrong happened
try {
const result = await response.json();
const errorMessage = JSON.stringify(result, null, 2);
if (!response?.status?.toString()?.startsWith('2')) {
let errorMessage;

try {
// Response might contain JSON or plain text, attempt to stringify JSON, will fail if no valid JSON found
const result = await response.json();
errorMessage = JSON.stringify(result, null, 2);

Sentry.captureException(new Error(errorMessage));
logger.error(errorMessage);
} catch (e) {
// Stringifying JSON failed, attempt to retrieve the plain text error message
Sentry.captureException(e);
logger.error(e);

errorMessage = await response.text();
Sentry.captureException(errorMessage);
logger.error(errorMessage);
} finally {
Sentry.withScope((scope): void => {
scope.setTag('alertType', ALERT_TYPES.VERCEL_DEPLOYMENT_TRIGGER_ATTEMPT_FAILED);

Sentry.captureException(new Error(errorMessage));
logger.error(errorMessage);
} catch (e) {
Sentry.captureException(e);
logger.error(e);
Sentry.captureEvent({
message: `Failed to trigger a Vercel deployment using "${workflowFilePath}" with version "${platformReleaseRef}". Error: "${errorMessage}"`,
level: Sentry.Severity.Error,
});
});
}
} else {
Sentry.withScope((scope): void => {
scope.setTag('alertType', ALERT_TYPES.VERCEL_DEPLOYMENT_TRIGGER_ATTEMPT_SUCCEEDED);

const result = await response.text();
Sentry.captureException(result);
logger.error(result);
Sentry.captureEvent({
message: `Successfully triggered a Vercel deployment using "${workflowFilePath}" with version "${platformReleaseRef}".`,
level: Sentry.Severity.Log,
});
});
}
} else {
const errorMessage = `No GitHub Actions workflow could be found for file path: "${workflowFilePath}"`;
Sentry.captureException(new Error(errorMessage));
logger.error(errorMessage);
}
} else {
const errorMessage = `No GitHub Actions workflow could be found for file path: "${workflowFilePath}"`;
Sentry.captureException(new Error(errorMessage));
logger.error(errorMessage);
} catch (e) {
Sentry.captureException(e);
logger.error(e);
}
};

Expand Down
Loading

1 comment on commit 38713b4

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.