Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: ♻️ πŸ” πŸ”Š πŸ§‘β€πŸ’» Add Environment Variables and Convert Logging to Use Logwatch #119

Merged
merged 3 commits into from
Dec 12, 2024
Merged
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
3 changes: 3 additions & 0 deletions .github/workflows/deploy-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ jobs:
KAMAL_SERVER_IP: ${{ secrets.KAMAL_SERVER_IP }}
KAMAL_BOT_DOMAIN: ${{ secrets.KAMAL_BOT_DOMAIN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
BOT_LOGWATCH_URL: ${{ secrets.BOT_LOGWATCH_URL }}
APP_ID: ${{ secrets.APP_ID }}
GH_APP_ID: ${{ secrets.GH_APP_ID }}
GH_APP_NAME: ${{ secrets.GH_APP_NAME }}
Expand All @@ -36,6 +37,7 @@ jobs:
CODEFAIR_APP_DOMAIN: ${{ secrets.CODEFAIR_APP_DOMAIN }}
ZENODO_API_ENDPOINT: ${{ secrets.ZENODO_API_ENDPOINT }}
ZENODO_ENDPOINT: ${{ secrets.ZENODO_ENDPOINT }}
VALIDATOR_URL: ${{ secrets.VALIDATOR_URL }}

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -99,6 +101,7 @@ jobs:
GH_OAUTH_APP_ID: ${{ secrets.GH_OAUTH_APP_ID }}
GH_OAUTH_CLIENT_ID: ${{ secrets.GH_OAUTH_CLIENT_ID }}
GH_OAUTH_CLIENT_SECRET: ${{ secrets.GH_OAUTH_CLIENT_SECRET }}
UI_LOGWATCH_URL: ${{ secrets.UI_LOGWATCH_URL }}
ZENODO_API_ENDPOINT: ${{ secrets.ZENODO_API_ENDPOINT }}
ZENODO_ENDPOINT: ${{ secrets.ZENODO_ENDPOINT }}
ZENODO_CLIENT_ID: ${{ secrets.ZENODO_CLIENT_ID }}
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/deploy-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ jobs:
KAMAL_SERVER_IP: ${{ secrets.KAMAL_SERVER_IP }}
KAMAL_BOT_DOMAIN: ${{ secrets.KAMAL_BOT_DOMAIN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
BOT_LOGWATCH_URL: ${{ secrets.BOT_LOGWATCH_URL }}
APP_ID: ${{ secrets.APP_ID }}
GH_APP_ID: ${{ secrets.GH_APP_ID }}
GH_APP_NAME: ${{ secrets.GH_APP_NAME }}
Expand All @@ -36,6 +37,7 @@ jobs:
CODEFAIR_APP_DOMAIN: ${{ secrets.CODEFAIR_APP_DOMAIN }}
ZENODO_API_ENDPOINT: ${{ secrets.ZENODO_API_ENDPOINT }}
ZENODO_ENDPOINT: ${{ secrets.ZENODO_ENDPOINT }}
VALIDATOR_URL: ${{ secrets.VALIDATOR_URL }}

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -99,6 +101,7 @@ jobs:
GH_OAUTH_APP_ID: ${{ secrets.GH_OAUTH_APP_ID }}
GH_OAUTH_CLIENT_ID: ${{ secrets.GH_OAUTH_CLIENT_ID }}
GH_OAUTH_CLIENT_SECRET: ${{ secrets.GH_OAUTH_CLIENT_SECRET }}
UI_LOGWATCH_URL: ${{ secrets.UI_LOGWATCH_URL }}
ZENODO_API_ENDPOINT: ${{ secrets.ZENODO_API_ENDPOINT }}
ZENODO_ENDPOINT: ${{ secrets.ZENODO_ENDPOINT }}
ZENODO_CLIENT_ID: ${{ secrets.ZENODO_CLIENT_ID }}
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ All notable changes the Codefair App will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## v3.2.1 - 12-12-2024

### Added

- Convert logging from 'consola' to 'logwatch' for improved log management and consistency across the application.

### Fixed

- Update CI and deployment workflows to include new environment variables 'BOT_LOGWATCH_URL' and 'VALIDATOR_URL'.
- Patch to Zenodo workflow that was causing the user to be notified of a failed Zenodo upload when the upload was successful.

## v3.2.0 - 12-10-2024

### Added
Expand Down
2 changes: 2 additions & 0 deletions bot/.kamal/secrets
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ KAMAL_SERVER_IP=$KAMAL_SERVER_IP
# bot secrets
APP_ID=$APP_ID
DATABASE_URL=$DATABASE_URL
BOT_LOGWATCH_URL=$BOT_LOGWATCH_URL
GH_APP_ID=$GH_APP_ID
GH_APP_NAME=$GH_APP_NAME
GH_APP_CLIENT_ID=$GH_APP_CLIENT_ID
Expand All @@ -24,6 +25,7 @@ WEBHOOK_SECRET=$WEBHOOK_SECRET
CODEFAIR_APP_DOMAIN=$CODEFAIR_APP_DOMAIN
ZENODO_API_ENDPOINT=$ZENODO_API_ENDPOINT
ZENODO_ENDPOINT=$ZENODO_ENDPOINT
VALIDATOR_URL=$VALIDATOR_URL

# Option 2: Read secrets via a command
# RAILS_MASTER_KEY=$(cat config/master.key)
Expand Down
40 changes: 20 additions & 20 deletions bot/archival/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import dbInstance from '../db.js';
import { consola } from 'consola';
import { logwatch } from '../utils/logwatch.js';
import fs from 'fs';
const licensesJson = JSON.parse(fs.readFileSync('./public/assets/data/licenses.json', 'utf8'));

Expand All @@ -20,7 +21,7 @@ export async function updateGitHubRelease(context, repositoryName, owner, releas
release_id: releaseId,
draft: false,
});
consola.success("Updated release to not be a draft!");
logwatch.success("Updated release to not be a draft!");
} catch (error) {
throw new Error(`Error updating the GitHub release: ${error}`, { cause: error });
}
Expand All @@ -33,7 +34,7 @@ export async function updateGitHubRelease(context, repositoryName, owner, releas
*/
export async function publishZenodoDeposition(zenodoToken, depositionId) {
try {
consola.start("Publishing the Zenodo deposition...", depositionId);
logwatch.start(`Publishing the Zenodo deposition: ${depositionId}`);
const publishDeposition = await fetch(
`${ZENODO_API_ENDPOINT}/deposit/depositions/${depositionId}/actions/publish`,
{
Expand All @@ -50,7 +51,7 @@ export async function publishZenodoDeposition(zenodoToken, depositionId) {
}

const publishedDeposition = await publishDeposition.json();
consola.success("Zenodo deposition published successfully at:", publishedDeposition.links.latest_html);
logwatch.success(`Zenodo deposition published successfully at: ${publishedDeposition.links.latest_html}`);
Comment on lines 51 to +54
Copy link

Choose a reason for hiding this comment

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

suggestion (bug_risk): Add response status code check before parsing JSON response

Should verify publishDeposition.ok before attempting to parse JSON to avoid potential runtime errors with malformed responses.

Suggested change
}
const publishedDeposition = await publishDeposition.json();
consola.success("Zenodo deposition published successfully at:", publishedDeposition.links.latest_html);
logwatch.success(`Zenodo deposition published successfully at: ${publishedDeposition.links.latest_html}`);
const publishDeposition = await fetch(
`${ZENODO_API_ENDPOINT}/deposit/depositions/${depositionId}/actions/publish`,
{
}
if (!publishDeposition.ok) {
const errorText = await publishDeposition.text();
throw new Error(`Failed to publish Zenodo deposition. Status: ${publishDeposition.status}: ${publishDeposition.statusText}`, { cause: errorText });
}
const publishedDeposition = await publishDeposition.json();
logwatch.success(`Zenodo deposition published successfully at: ${publishedDeposition.links.latest_html}`);

} catch (error) {
throw new Error(`Error publishing the Zenodo deposition: ${error.message}`, { cause: error });
}
Expand Down Expand Up @@ -251,7 +252,7 @@ export async function createNewVersionOfDeposition(zenodoToken, depositionId) {
const errorText = await zenodoRecord.text();
throw new Error(`Failed to create a new version of Zenodo deposition. Status: ${zenodoRecord.status}: ${zenodoRecord.statusText}.`, { cause: errorText});
}
consola.success("New version of Zenodo deposition created successfully!");
logwatch.success("New version of Zenodo deposition created successfully!");

const responseText = await zenodoRecord.json();

Expand All @@ -265,7 +266,7 @@ export async function createNewVersionOfDeposition(zenodoToken, depositionId) {
});

if (!draftZenodoRecord.ok) {
consola.error("Error fetching the latest draft of Zenodo deposition:", draftZenodoRecord);
logwatch.error({message: "Error fetching the latest draft of Zenodo deposition:", fetchReponse: draftZenodoRecord}, true);
const errorText = await draftZenodoRecord.text();
throw new Error(`Failed to fetch the latest draft of Zenodo deposition. Status: ${draftZenodoRecord.status}: ${draftZenodoRecord.statusText}. Error: ${errorText}`, { cause: errorText });
}
Expand Down Expand Up @@ -295,7 +296,7 @@ export async function getZenodoDepositionInfo(

if (zenodoDepositionInfo.submitted === false){
// Delete the files in the draft
consola.start("Requested deposition is a draft. Deleting the files in the draft...");
logwatch.start("Requested deposition is a draft. Deleting the files in the draft...");
for (const file of zenodoDepositionInfo.files) {
await deleteFileFromZenodo(depositionId, zenodoToken, file.id);
}
Expand All @@ -307,12 +308,12 @@ export async function getZenodoDepositionInfo(

if (newZenodoVersion.files.length > 0) {
for (const file of newZenodoVersion.files) {
consola.start("Deleting file from newly created draft:", file.links.download);
logwatch.start(`Deleting file from newly created draft: ${file.links.download}`);
await deleteFileFromZenodo(newZenodoVersion.id, zenodoToken, file.id);
}
}

consola.success("New draft version of Zenodo deposition created successfully!");
logwatch.success("New draft version of Zenodo deposition created successfully!");
return newZenodoVersion;
}
}
Expand Down Expand Up @@ -354,8 +355,8 @@ export async function createZenodoMetadata(codemetadata, repository) {
});
if (!codeMetaContent.license) {
// fetch from the db
consola.warn(`No license found in the codemeta.json file. Fetching from the database...`);
consola.info(`License found in the database: ${existingLicense?.license_id}`);
logwatch.warn(`No license found in the codemeta.json file. Fetching from the database...`);
logwatch.info(`License found in the database: ${existingLicense?.license_id}`);
codeMetaContent.license = `https://spdx.org/licenses/${existingLicense?.license_id}`;
}
const license = licensesJson.find((license) => license.detailsUrl === `${codeMetaContent.license}.json`);
Expand All @@ -372,7 +373,7 @@ export async function createZenodoMetadata(codemetadata, repository) {
})

if (!zenodoMetadata) {
consola.error("Zenodo metadata not found in the database. Please create a new Zenodo deposition.");
logwatch.error("Zenodo metadata not found in the database. Please create a new Zenodo deposition.");
throw new Error("Zenodo metadata not found in the database. Please create a new Zenodo deposition.");
}

Expand Down Expand Up @@ -440,7 +441,7 @@ export async function updateZenodoMetadata(depositionId, zenodoToken, metadata)
);

const updatedMetadataInfo = await updatedMetadata.json();
consola.success("Zenodo deposition metadata updated successfully!");
logwatch.success("Zenodo deposition metadata updated successfully!");
return updatedMetadataInfo;
} catch (error) {
throw new Error(`Error updating Zenodo metadata: ${error}`, { cause: error });
Expand Down Expand Up @@ -480,7 +481,7 @@ export async function uploadReleaseAssetsToZenodo(
accept: 'application/octet-stream'
}
});
consola.success(`Asset data fetched for ${asset.name}, for the release ${tagVersion}, from the GitHub repository: ${repository.name}`);
logwatch.success(`Asset data fetched for ${asset.name}, for the release ${tagVersion}, from the GitHub repository: ${repository.name}`);

// Upload the file to Zenodo
const uploadAsset = await fetch(`${bucket_url}/${asset.name}`,
Expand All @@ -493,9 +494,9 @@ export async function uploadReleaseAssetsToZenodo(
});

if (!uploadAsset.ok) {
consola.error(`Failed to upload ${asset.name}. Status: ${uploadAsset.statusText}. Error: ${uploadAsset}`);
logwatch.error(`Failed to upload ${asset.name}. Status: ${uploadAsset.statusText}. Error: ${uploadAsset}`);
} else {
consola.success(`${asset.name} successfully uploaded to Zenodo!`);
logwatch.success(`${asset.name} successfully uploaded to Zenodo!`);
}
} catch (error) {
throw new Error(`Error uploading assets to Zenodo: ${error}`, { cause: error });
Expand All @@ -516,13 +517,13 @@ export async function uploadReleaseAssetsToZenodo(
);

if (!uploadZip.ok) {
consola.error(`Failed to upload zip file. Status: ${uploadZip.statusText}`);
logwatch.error(`Failed to upload zip file. Status: ${uploadZip.statusText}`);
throw new Error(`Failed to upload zip file. Status: ${uploadZip.statusText}`);
}

const endTime = performance.now();
consola.info(`Total duration to upload assets and zip to Zenodo deposition: ${(endTime - startTime) / 1000} seconds`);
consola.success("Zip file successfully uploaded to Zenodo!");
logwatch.info(`Total duration to upload assets and zip to Zenodo deposition: ${(endTime - startTime) / 1000} seconds`);
logwatch.success("Zip file successfully uploaded to Zenodo!");
}

/**
Expand All @@ -543,12 +544,11 @@ export async function deleteFileFromZenodo(depositionId, zenodoToken, fileId) {
);

if (!deleteFile.ok) {
consola.error(deleteFile);
const errorText = await deleteFile.text();
throw new Error(`Failed to delete file from Zenodo. Status: ${deleteFile.status}: ${deleteFile.statusText}. Error: ${errorText}`);
}

consola.success("File successfully deleted from Zenodo!");
logwatch.success("File successfully deleted from Zenodo!");
} catch (error) {
throw new Error(`Error deleting file from Zenodo: ${error}`, { cause: error });
}
Expand Down
2 changes: 2 additions & 0 deletions bot/config/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ env:
- CODEFAIR_APP_DOMAIN
- ZENODO_API_ENDPOINT
- ZENODO_ENDPOINT
- VALIDATOR_URL
- BOT_LOGWATCH_URL
clear:
NODE_ENV: production

Expand Down
41 changes: 22 additions & 19 deletions bot/cwl/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* * This file contains the functions to interact with the CWL files in the repository
*/
import { consola } from "consola";
import { logwatch } from "../utils/logwatch.js";
import {
isRepoPrivate,
createId,
Expand All @@ -10,6 +11,7 @@ import {
import dbInstance from "../db.js";

const CODEFAIR_DOMAIN = process.env.CODEFAIR_APP_DOMAIN;
const { VALIDATOR_URL } = process.env;

/**
* * This function gets the CWL files in the repository
Expand All @@ -20,7 +22,7 @@ const CODEFAIR_DOMAIN = process.env.CODEFAIR_APP_DOMAIN;
*/
export function getCWLFiles(context, owner, repository) {
return new Promise((resolve, reject) => {
consola.info("Checking for CWL files in the repository...");
logwatch.info("Checking for CWL files in the repository...");

const cwlFiles = [];
const cwlObject = {
Expand Down Expand Up @@ -51,9 +53,12 @@ export function getCWLFiles(context, owner, repository) {
resolve(cwlObject);
return;
}
consola.error(
"Error finding CWL files throughout the repository:",
error,
logwatch.error(
{
message: "Error finding CWL files throughout the repository:",
error,
},
true
);
reject(error);
}
Expand Down Expand Up @@ -93,7 +98,7 @@ export function getCWLFiles(context, owner, repository) {
*/
export async function validateCWLFile(downloadUrl) {
try {
const response = await fetch("https://cwl-validate.codefair.io/validate-cwl", {
const response = await fetch(`${VALIDATOR_URL}/validate-cwl`, {
Comment on lines 99 to +101
Copy link

Choose a reason for hiding this comment

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

🚨 suggestion (security): Add URL validation for VALIDATOR_URL environment variable

Should validate VALIDATOR_URL is a properly formatted URL before using it in fetch calls.

Suggested change
export async function validateCWLFile(downloadUrl) {
try {
const response = await fetch("https://cwl-validate.codefair.io/validate-cwl", {
const response = await fetch(`${VALIDATOR_URL}/validate-cwl`, {
function isValidUrl(string) {
try {
new URL(string);
return true;
} catch (_) {
return false;
}
}
export async function validateCWLFile(downloadUrl) {
try {
if (!VALIDATOR_URL || !isValidUrl(VALIDATOR_URL)) {
logwatch.error({message: "Invalid VALIDATOR_URL environment variable"}, true);
return [false, "Configuration error: Invalid validator URL"];
}
const response = await fetch(`${VALIDATOR_URL}/validate-cwl`, {

body: JSON.stringify({
file_path: downloadUrl,
}),
Expand All @@ -108,15 +113,15 @@ export async function validateCWLFile(downloadUrl) {
return [false, error.error];
}
if (!response.ok && response.status === 500) {
consola.error("Error validating CWL file:", response);
logwatch.error({message: "Error validating CWL file:", validation_response: response}, true);
return [false, "Error validating CWL file"];
}
if (response.ok) {
const data = await response.json();
return [true, data.output];
}
} catch (e) {
consola.error("Error validating CWL file:", e);
logwatch.error({message: "Error validating CWL file:", error: e}, true);
return [false, "Error validating CWL file"];
}
}
Expand Down Expand Up @@ -180,14 +185,14 @@ export async function applyCWLTemplate(
});

if (subjects.cwl.files.length === 0) {
consola.warn(
`No new/modified CWL files found in the repository, ${repository.name}`,
logwatch.warn(
`No CWL files found in the repository, ${repository.name}`,
);
}

consola.start("Validating CWL files for", repository.name);
logwatch.start("Validating CWL files for", repository.name);
// Validate each CWL file from list\
consola.info(`Validating ${JSON.stringify(subjects.cwl)} CWL files`);
logwatch.info(`Validating ${JSON.stringify(subjects.cwl)} CWL files`);
if (subjects.cwl.files.length > 0) {
for (const file of subjects.cwl.files) {
const fileSplit = file.name.split(".");
Expand Down Expand Up @@ -252,7 +257,7 @@ export async function applyCWLTemplate(
// Add the file to the table content of the issue dashboard
tableContent += `| ${file.path} | ${isValidCWL ? "βœ”οΈ" : "❌"} |\n`;

consola.success(
logwatch.success(
`File: ${file.path} is ${isValidCWL ? "valid" : "invalid"}`,
);
}
Expand All @@ -276,7 +281,7 @@ export async function applyCWLTemplate(
});

if (!cwlFiles.length > 0) {
consola.warn(
logwatch.warn(
`No CWL files found in the repository, ${repository.name}, skipping CWL section`,
);
return baseTemplate;
Expand Down Expand Up @@ -317,15 +322,13 @@ export async function applyCWLTemplate(

if (!newFiles.length > 0) {
// All CWL files were removed from the repository
consola.warn(
"All CWL files were removed from:",
repository.name,
"skipping CWL section",
logwatch.warn(
`All CWL files were removed from: ${repository.name}, skipping CWL section`
);
return baseTemplate;
} else {
// Recreate the table content to include the new and old cwl files
consola.start(
logwatch.start(
"Recreating the table content for the CWL section to include new and old files",
);
tableContent = "";
Expand All @@ -349,6 +352,6 @@ export async function applyCWLTemplate(
const cwlBadge = `[![CWL](https://img.shields.io/badge/View_CWL_Report-0ea5e9.svg)](${url})`;
baseTemplate += `${overallSection}\n\n### CWL Validations ${validOverall ? "βœ”οΈ" : "❗"}\n\nCodefair has detected that you are following the Common Workflow Language (CWL) standard to describe your command line tool. Codefair ran the [cwltool validator](https://cwltool.readthedocs.io/en/latest/) and ${validOverall ? `all ***${subjects.cwl.files.length}*** CWL file(s) in your repository are valid.` : `***${failedCount}/${subjects.cwl.files.length}*** CWL file(s) in your repository are not valid.`}\n\n<details>\n<summary>Summary of the validation report</summary>\n\n| File | Validation result |\n| :---- | :----: |\n${tableContent}</details>\n\nTo view the full report of each CWL file or to rerun the validation, click the "View CWL Report" button below.\n\n${cwlBadge}`;

consola.success("CWL template section applied");
logwatch.success("CWL template section applied");
return baseTemplate;
}
Loading