Skip to content
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
2 changes: 1 addition & 1 deletion .github/workflows/Security-Reachability.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ jobs:
# validate:package:skip-reachability command so workflow validation does not modify its checkout.
- name: Install Socket CLI
background: true
run: sfw pip install socketsecurity==2.5.7 uv --upgrade
run: sfw pip install socketsecurity==2.7.2 uv --upgrade

# Bring job back to sync execution by awaiting for all async jobs to finish before continuing
- name: Steps - Convert Back To Synchronous Execution - Packages Updates/Setup
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/Test-Unit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:

# Promote the coverage summary into the GitHub Actions job summary for easier PR review
- name: Publish Coverage Summary
run: node ./scripts/publish-coverage-summary.mjs
run: node ./scripts/publish-coverage-summary.ts

Comment thread
pr0uxx marked this conversation as resolved.
# Create or update one coverage comment for pull request runs.
- name: Publish Coverage Report to Pull Request
Expand All @@ -58,7 +58,7 @@ jobs:
with:
script: |
const { pathToFileURL } = require('node:url');
const commentScriptUrl = pathToFileURL(`${process.env.GITHUB_WORKSPACE}/scripts/publish-coverage-comment.mjs`);
const commentScriptUrl = pathToFileURL(`${process.env.GITHUB_WORKSPACE}/scripts/publish-coverage-comment.ts`);
const { default: publishCoverageComment } = await import(commentScriptUrl.href);
await publishCoverageComment({
github,
Expand Down
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ eslint.config.mjs
vitest.config.ts
package-lock.json
.npmrc
.gitattributes

# Dependencies and logs
/node_modules
Expand Down
12 changes: 9 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,25 @@
"type": "git",
"url": "git+https://github.com/Software-Hardware-Integration-Lab/LogEngine.git"
},
"exports": {
".": {
"types": "./bin/index.d.ts",
"import": "./bin/index.js"
}
},
"type": "module",
"scripts": {
"build": "npm run clean:bin && tspc -p tsconfig.json",
"build:coverage": "npm run clean:bin && tspc -p coverage.tsconfig.json",
"build:prod": "npm run clean:bin && tspc -p prod.tsconfig.json",
"clean:bin": "node ./scripts/clean-bin.mjs",
"clean:bin": "node ./scripts/clean-bin.ts",
"lint": "eslint",
Comment thread
pr0uxx marked this conversation as resolved.
"test": "npm run build:coverage && vitest run",
"test:watch": "vitest",
"coverage": "npm run build:coverage && vitest run --coverage",
"validate:package": "npm run update:reachability-pin && npm run validate:package:skip-reachability",
"validate:package:skip-reachability": "npm run lint && npm run coverage && npm run build:prod && node ./scripts/validate-package.mjs",
"update:reachability-pin": "node ./scripts/update-reachability-pin.mjs",
"validate:package:skip-reachability": "npm run lint && npm run coverage && npm run build:prod && node ./scripts/validate-package.ts",
"update:reachability-pin": "node ./scripts/update-reachability-pin.ts",
"prepack": "npm run validate:package",
"postinstall": "ts-patch install -s"
},
Expand Down
File renamed without changes.
7 changes: 0 additions & 7 deletions scripts/coverage-thresholds.d.ts

This file was deleted.

File renamed without changes.
44 changes: 0 additions & 44 deletions scripts/publish-coverage-comment.mjs

This file was deleted.

121 changes: 121 additions & 0 deletions scripts/publish-coverage-comment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { readFile } from 'node:fs/promises';

/** Represents a GitHub issue comment. */
interface IssueComment {
/** Gets the comment body. */
'body'?: string | null;
/** Gets the comment identifier. */
'id': number;
}

/** Defines the GitHub Script inputs needed to publish a coverage comment. */
interface PublishCoverageCommentOptions {
/** Provides GitHub Actions workflow context. */
'context': {
/** Provides pull request or issue details. */
'issue': {
/** Gets the issue number. */
'number': number;
};
/** Provides the target repository details. */
'repo': {
/** Gets the repository owner. */
'owner': string;
/** Gets the repository name. */
'repo': string;
};
};
/** Provides the GitHub REST client. */
'github': {
/** Retrieves all pages for a REST endpoint. */
'paginate': (
route: unknown,
parameters: {
/** Specifies the issue number. */
'issue_number': number;
/** Specifies the repository owner. */
'owner': string;
/** Specifies the number of results per page. */
'per_page': number;
/** Specifies the repository name. */
'repo': string;
}
) => Promise<IssueComment[]>;
/** Exposes GitHub REST endpoint groups. */
'rest': {
/** Exposes issue and issue-comment endpoints. */
'issues': {
/** Creates an issue comment. */
'createComment': (parameters: {
/** Specifies the comment body. */
'body': string;
/** Specifies the issue number. */
'issue_number': number;
/** Specifies the repository owner. */
'owner': string;
/** Specifies the repository name. */
'repo': string;
}) => Promise<unknown>;
/** Lists issue comments. */
'listComments': unknown;
/** Updates an issue comment. */
'updateComment': (parameters: {
/** Specifies the comment body. */
'body': string;
/** Specifies the comment identifier. */
'comment_id': number;
/** Specifies the repository owner. */
'owner': string;
/** Specifies the repository name. */
'repo': string;
}) => Promise<unknown>;
};
};
};
/** Specifies the path to the generated coverage summary. */
'summaryPath': string;
}

/** Identifies coverage comments managed by this script. */
const marker = '<!-- log-engine-coverage-report -->';
/**
* Publishes or updates the coverage summary comment on the current issue.
* @param options Provides the GitHub Script inputs.
*/
export default async function publishCoverageComment(options: PublishCoverageCommentOptions) {
/** Gets the GitHub Script inputs. */
const { github, context, summaryPath } = options;
/** Gets the Markdown coverage summary. */
const coverageSummary = (await readFile(summaryPath, 'utf8')).trim();
/** Gets the managed coverage comment body. */
const body = `${ marker }\n${ coverageSummary }`;
/** Gets the target repository owner and name. */
const { owner, repo } = context.repo;
/** Gets the issue number for the coverage comment. */
const issueNumber = context.issue.number;
/** Gets all existing comments on the current issue. */
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
'issue_number': issueNumber,
'per_page': 100
});
/** Gets the existing managed coverage comment, when present. */
const existingComment = comments.find((comment) => comment.body?.includes(marker) ?? false);
if (existingComment) {
await github.rest.issues.updateComment({
owner,
repo,
'comment_id': existingComment.id,
body
});
}
else {
await github.rest.issues.createComment({
owner,
repo,
'issue_number': issueNumber,
body
});
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { appendFile, readFile, writeFile } from 'node:fs/promises';
import { coverageThresholds } from './coverage-thresholds.js';
import { coverageThresholds } from './coverage-thresholds.ts';
/** Specifies the source coverage summary path. */
const coverageSummaryPath = 'coverage/coverage-summary.json';
/** Specifies the generated Markdown summary path. */
const summaryOutputPath = 'coverage-summary.md';
/** Gets the aggregate coverage metrics from the summary. */
const { total } = JSON.parse(await readFile(coverageSummaryPath, 'utf8'));
/** Lists the coverage metrics to include in the report. */
const metrics = ['statements', 'branches', 'functions', 'lines'];
const metrics: (keyof typeof coverageThresholds)[] = [
'statements', 'branches', 'functions', 'lines'
];
/** Formats the coverage metrics for the Markdown report. */
const coverageMetrics = metrics.map((name) => {
/** Gets the coverage data for the current metric. */
const metric = total[name];
if (!metric) {
throw new Error(`Missing ${name} coverage metric.`);
throw new Error(`Missing ${ name } coverage metric.`);
}
/** Returns a report row for the current coverage metric. */
return {
Expand All @@ -28,11 +30,11 @@ const coverageMetrics = metrics.map((name) => {
const meetsThresholds = coverageMetrics.every((metric) => metric.percentage >= metric.threshold);
/** Gets the Markdown coverage report. */
const summary = [
`## ${meetsThresholds ? '🟢' : '😡'} Coverage Report`,
`## ${ meetsThresholds ? '🟢' : '😡' } Coverage Report`,
'',
'| Metric | Coverage | Threshold | Covered |',
'| --- | ---: | ---: | ---: |',
...coverageMetrics.map((metric) => `| ${metric.name} | ${metric.percentage}% | ${metric.threshold}% | ${metric.covered}/${metric.total} |`),
...coverageMetrics.map((metric) => `| ${ metric.name } | ${ metric.percentage }% | ${ metric.threshold }% | ${ metric.covered }/${ metric.total } |`),
''
].join('\n');
await writeFile(summaryOutputPath, summary);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
import { readFile, writeFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';

/** Represents a release file returned by PyPI. */
interface PyPiReleaseFile {
/** Gets the file upload time. */
'upload_time_iso_8601': string;
/** Indicates whether the file has been withdrawn. */
'yanked': boolean;
}

/** Represents the portion of the PyPI response consumed by this script. */
interface PyPiResponse {
/** Gets release files indexed by version. */
'releases': Record<string, PyPiReleaseFile[]>;
}

/** Specifies the PyPI endpoint for socketsecurity releases. */
const PYPI_URL = 'https://pypi.org/pypi/socketsecurity/json';
/** Specifies the minimum release age before a version is eligible. */
Expand All @@ -16,7 +31,7 @@ const pinPattern = /socketsecurity==(?<version>\d+(?:\.\d+)*)/gu;
* @param right Specifies the right version.
* @returns A negative value, zero, or a positive value according to version ordering.
*/
function compareVersions(left, right) {
function compareVersions(left: string, right: string): number {
/** Gets the numeric components of the left version. */
const leftParts = left.split('.').map(Number);
/** Gets the numeric components of the right version. */
Expand All @@ -35,23 +50,23 @@ function compareVersions(left, right) {
/** Gets the PyPI response for socketsecurity releases. */
const response = await fetch(PYPI_URL);
if (!response.ok) {
throw new Error(`Unable to retrieve socketsecurity releases from PyPI: ${response.status} ${response.statusText}`);
throw new Error(`Unable to retrieve socketsecurity releases from PyPI: ${ response.status } ${ response.statusText }`);
}
/** Gets release files indexed by socketsecurity version. */
const { releases } = await response.json();
const { releases } = await response.json() as PyPiResponse;
/** Gets the latest timestamp at which a release is eligible. */
const cutoffTime = Date.now() - RELEASE_AGE_MILLISECONDS;
/** Gets the newest non-yanked stable releases old enough to use. */
const eligibleReleases = Object.entries(releases)
.filter(([version, files]) => versionPattern.test(version) && Array.isArray(files) && files.length > 0 && files.every((file) => !file.yanked))
.map(([version, files]) => {
/** Gets the most recent upload time for the release. */
const mostRecentUpload = Math.max(...files.map((file) => Date.parse(file.upload_time_iso_8601)));
return {
mostRecentUpload,
version
};
})
/** Gets the most recent upload time for the release. */
const mostRecentUpload = Math.max(...files.map((file) => Date.parse(file.upload_time_iso_8601)));
return {
mostRecentUpload,
version
};
})
.filter(({ mostRecentUpload }) => Number.isFinite(mostRecentUpload) && mostRecentUpload <= cutoffTime)
.sort((left, right) => compareVersions(right.version, left.version));
/** Gets the newest eligible release. */
Expand All @@ -64,19 +79,19 @@ const workflow = await readFile(workflowPath, 'utf8');
/** Gets all socketsecurity pins in the workflow. */
const pins = [...workflow.matchAll(pinPattern)];
if (pins.length !== 1) {
throw new Error(`Expected exactly one socketsecurity pin in ${workflowPath}, found ${pins.length}.`);
throw new Error(`Expected exactly one socketsecurity pin in ${ workflowPath }, found ${ pins.length }.`);
}
/** Gets the socketsecurity version captured from the workflow pin. */
const currentVersion = pins[0]?.groups?.['version'];
if (!currentVersion) {
throw new Error(`Unable to extract the socketsecurity version from ${workflowPath}.`);
throw new Error(`Unable to extract the socketsecurity version from ${ workflowPath }.`);
}
if (currentVersion === latestEligibleRelease.version) {
process.stdout.write(`Reachability pin is current: socketsecurity==${currentVersion}\n`);
process.stdout.write(`Reachability pin is current: socketsecurity==${ currentVersion }\n`);
}
else {
/** Gets the workflow content with the updated socketsecurity pin. */
const updatedWorkflow = workflow.replace(pinPattern, `socketsecurity==${latestEligibleRelease.version}`);
const updatedWorkflow = workflow.replace(pinPattern, `socketsecurity==${ latestEligibleRelease.version }`);
await writeFile(workflowPath, updatedWorkflow);
process.stdout.write(`Updated reachability pin: socketsecurity==${currentVersion} -> socketsecurity==${latestEligibleRelease.version}\n`);
process.stdout.write(`Updated reachability pin: socketsecurity==${ currentVersion } -> socketsecurity==${ latestEligibleRelease.version }\n`);
}
Loading