Merged
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
182 changes: 97 additions & 85 deletions dev-packages/size-limit-gh-action/index.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,12 +31,27 @@ class SizeLimit {
return bytes.format(size, { unitSeparator: ' ' });
}

formatTime(seconds) {
if (seconds >= 1) {
return `${Math.ceil(seconds * 10) / 10} s`;
formatPercentageChange(base = 0, current = 0) {
if (base === 0) {
return 'added';
}

if (current === 0) {
return 'removed';
}

const value = ((current - base) / base) * 100;
const formatted = (Math.sign(value) * Math.ceil(Math.abs(value) * 100)) / 100;

if (value > 0) {
return `+${formatted}%`;
}

if (value === 0) {
return '-';
}

return `${Math.ceil(seconds * 1000)} ms`;
return `${formatted}%`;
}

formatChange(base = 0, current = 0) {
Expand All@@ -48,75 +63,50 @@ class SizeLimit {
return 'removed';
}

const value = ((current - base) / base) * 100;
const formatted = (Math.sign(value) * Math.ceil(Math.abs(value) * 100)) / 100;
const value = current - base;
const formatted = this.formatBytes(value);

if (value > 0) {
return `+${formatted}% 🔺`;
return `+${formatted} 🔺`;
}

if (value === 0) {
return `${formatted}%`;
return '-';
}

return `${formatted}% 🔽`;
return `${formatted} 🔽`;
}

formatLine(value, change) {
return `${value} (${change})`;
}

formatSizeResult(name, base, current) {
return [name, this.formatLine(this.formatBytes(current.size), this.formatChange(base.size, current.size))];
}

formatTimeResult(name, base, current) {
return [
name,
this.formatLine(this.formatBytes(current.size), this.formatChange(base.size, current.size)),
this.formatLine(this.formatTime(current.loading), this.formatChange(base.loading, current.loading)),
this.formatLine(this.formatTime(current.running), this.formatChange(base.running, current.running)),
this.formatTime(current.total),
this.formatBytes(current.size),
this.formatPercentageChange(base.size, current.size),
this.formatChange(base.size, current.size),
];
}

parseResults(output) {
const results = JSON.parse(output);

return results.reduce((current, result) => {
let time = {};

if (result.loading !== undefined && result.running !== undefined) {
const loading = +result.loading;
const running = +result.running;

time = {
running,
loading,
total: loading + running,
};
}

return {
// biome-ignore lint/performance/noAccumulatingSpread: <explanation>
...current,
[result.name]: {
name: result.name,
size: +result.size,
...time,
},
};
}, {});
}

hasSizeChanges(base, current, threshold = 0) {
const names = [...new Set([...(base ? Object.keys(base) : []), ...Object.keys(current)])];
const isSize = names.some(name => current[name] && current[name].total === undefined);

// Always return true if time results are present
if (!isSize) {
return true;
}

return !!names.find(name => {
const baseResult = base?.[name] || EmptyResult;
Expand All@@ -132,16 +122,12 @@ class SizeLimit {

formatResults(base, current) {
const names = [...new Set([...(base ? Object.keys(base) : []), ...Object.keys(current)])];
const isSize = names.some(name => current[name] && current[name].total === undefined);
const header = isSize ? SIZE_RESULTS_HEADER : TIME_RESULTS_HEADER;
const header = SIZE_RESULTS_HEADER;
const fields = names.map(name => {
const baseResult = base?.[name] || EmptyResult;
const currentResult = current[name] || EmptyResult;

if (isSize) {
return this.formatSizeResult(name, baseResult, currentResult);
}
return this.formatTimeResult(name, baseResult, currentResult);
return this.formatSizeResult(name, baseResult, currentResult);
});

return [header, ...fields];
Expand All@@ -165,15 +151,11 @@ async function execSizeLimit() {
return { status, output };
}

const SIZE_RESULTS_HEADER = ['Path', 'Size'];
const TIME_RESULTS_HEADER = ['Path', 'Size', 'Loading time (3g)', 'Running time (snapdragon)', 'Total time'];
const SIZE_RESULTS_HEADER = ['Path', 'Size', '% Change', 'Change'];

const EmptyResult = {
name: '-',
size: 0,
running: 0,
loading: 0,
total: 0,
};

async function run() {
Expand DownExpand Up@@ -227,6 +209,8 @@ async function run() {
// Else, we run size limit for the current branch, AND fetch it for the comparison branch
let base;
let current;
let baseIsNotLatest = false;
let baseWorkflowRun;

try {
const artifacts = await getArtifactsForBranchAndWorkflow(octokit, {
Expand All@@ -240,6 +224,8 @@ async function run() {
throw new Error('No artifacts found');
}

baseWorkflowRun = artifacts.workflowRun;

await downloadOtherWorkflowArtifact(octokit, {
...repo,
artifactName: ARTIFACT_NAME,
Expand All@@ -248,6 +234,11 @@ async function run() {
});

base = JSON.parse(await fs.readFile(resultsFilePath, { encoding: 'utf8' }));

if (!artifacts.isLatest) {
baseIsNotLatest = true;
core.info('Base artifact is not the latest one. This may lead to incorrect results.');
Comment thread
mydea marked this conversation as resolved.
}
} catch (error) {
core.startGroup('Warning, unable to find base results');
core.error(error);
Expand All@@ -271,7 +262,22 @@ async function run() {
isNaN(thresholdNumber) || limit.hasSizeChanges(base, current, thresholdNumber) || sizeLimitComment;

if (shouldComment) {
const body = [SIZE_LIMIT_HEADING, markdownTable(limit.formatResults(base, current))].join('\r\n');
const bodyParts = [SIZE_LIMIT_HEADING];

if (baseIsNotLatest) {
bodyParts.push(
'⚠️ **Warning:** Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.',
);
}

bodyParts.push(markdownTable(limit.formatResults(base, current)));

if (baseWorkflowRun) {
bodyParts.push('');
bodyParts.push(`[View base workflow run](${baseWorkflowRun.html_url})`);
}

const body = bodyParts.join('\r\n');

try {
if (!sizeLimitComment) {
Expand DownExpand Up@@ -320,7 +326,7 @@ const DEFAULT_PAGE_LIMIT = 10;
* This is a bit hacky since GitHub Actions currently does not directly
* support downloading artifacts from other workflows
*/
export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, workflowName, branch, artifactName }) {
async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, workflowName, branch, artifactName }) {
core.startGroup(`getArtifactsForBranchAndWorkflow - workflow:"${workflowName}", branch:"${branch}"`);

let repositoryWorkflow = null;
Expand DownExpand Up@@ -361,14 +367,13 @@ export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, w
const workflow_id = repositoryWorkflow.id;

let currentPage = 0;
const completedWorkflowRuns = [];
let latestWorkflowRun = null;

for await (const response of octokit.paginate.iterator(octokit.rest.actions.listWorkflowRuns, {
owner,
repo,
workflow_id,
branch,
status: 'completed',
per_page: DEFAULT_PAGE_LIMIT,
event: 'push',
})) {
Expand All@@ -379,12 +384,47 @@ export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, w
}

// Do not allow downloading artifacts from a fork.
completedWorkflowRuns.push(
...response.data.filter(workflowRun => workflowRun.head_repository.full_name === `${owner}/${repo}`),
);
const filtered = response.data.filter(workflowRun => workflowRun.head_repository.full_name === `${owner}/${repo}`);

// Sort to ensure the latest workflow run is the first
filtered.sort((a, b) => {
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
});

// Store the first workflow run, to determine if this is the latest one...
if (!latestWorkflowRun) {
latestWorkflowRun = filtered[0];
}

// Search through workflow artifacts until we find a workflow run w/ artifact name that we are looking for
for (const workflowRun of filtered) {
core.info(`Checking artifacts for workflow run: ${workflowRun.html_url}`);

if (completedWorkflowRuns.length) {
break;
const {
data: { artifacts },
} = await octokit.rest.actions.listWorkflowRunArtifacts({
owner,
repo,
run_id: workflowRun.id,
});

if (!artifacts) {
core.warning(
`Unable to fetch artifacts for branch: ${branch}, workflow: ${workflow_id}, workflowRunId: ${workflowRun.id}`,
);
} else {
const foundArtifact = artifacts.find(({ name }) => name === artifactName);
if (foundArtifact) {
core.info(`Found suitable artifact: ${foundArtifact.url}`);
return {
artifact: foundArtifact,
workflowRun,
isLatest: latestWorkflowRun.id === workflowRun.id,
};
} else {
core.info(`No artifact found for ${artifactName}, trying next workflow run...`);
}
}
}

if (currentPage > DEFAULT_MAX_PAGES) {
Expand All@@ -396,34 +436,6 @@ export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, w
currentPage++;
}

// Search through workflow artifacts until we find a workflow run w/ artifact name that we are looking for
for (const workflowRun of completedWorkflowRuns) {
core.info(`Checking artifacts for workflow run: ${workflowRun.html_url}`);

const {
data: { artifacts },
} = await octokit.rest.actions.listWorkflowRunArtifacts({
owner,
repo,
run_id: workflowRun.id,
});

if (!artifacts) {
core.warning(
`Unable to fetch artifacts for branch: ${branch}, workflow: ${workflow_id}, workflowRunId: ${workflowRun.id}`,
);
} else {
const foundArtifact = artifacts.find(({ name }) => name === artifactName);
if (foundArtifact) {
core.info(`Found suitable artifact: ${foundArtifact.url}`);
return {
artifact: foundArtifact,
workflowRun,
};
}
}
}

core.warning(`Artifact not found: ${artifactName}`);
core.endGroup();
return null;
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
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
182 changes: 97 additions & 85 deletions dev-packages/size-limit-gh-action/index.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,12 +31,27 @@ class SizeLimit {
return bytes.format(size, { unitSeparator: ' ' });
}

formatTime(seconds) {
if (seconds >= 1) {
return `${Math.ceil(seconds * 10) / 10} s`;
formatPercentageChange(base = 0, current = 0) {
if (base === 0) {
return 'added';
}

if (current === 0) {
return 'removed';
}

const value = ((current - base) / base) * 100;
const formatted = (Math.sign(value) * Math.ceil(Math.abs(value) * 100)) / 100;

if (value > 0) {
return `+${formatted}%`;
}

if (value === 0) {
return '-';
}

return `${Math.ceil(seconds * 1000)} ms`;
return `${formatted}%`;
}

formatChange(base = 0, current = 0) {
Expand All@@ -48,75 +63,50 @@ class SizeLimit {
return 'removed';
}

const value = ((current - base) / base) * 100;
const formatted = (Math.sign(value) * Math.ceil(Math.abs(value) * 100)) / 100;
const value = current - base;
const formatted = this.formatBytes(value);

if (value > 0) {
return `+${formatted}% 🔺`;
return `+${formatted} 🔺`;
}

if (value === 0) {
return `${formatted}%`;
return '-';
}

return `${formatted}% 🔽`;
return `${formatted} 🔽`;
}

formatLine(value, change) {
return `${value} (${change})`;
}

formatSizeResult(name, base, current) {
return [name, this.formatLine(this.formatBytes(current.size), this.formatChange(base.size, current.size))];
}

formatTimeResult(name, base, current) {
return [
name,
this.formatLine(this.formatBytes(current.size), this.formatChange(base.size, current.size)),
this.formatLine(this.formatTime(current.loading), this.formatChange(base.loading, current.loading)),
this.formatLine(this.formatTime(current.running), this.formatChange(base.running, current.running)),
this.formatTime(current.total),
this.formatBytes(current.size),
this.formatPercentageChange(base.size, current.size),
this.formatChange(base.size, current.size),
];
}

parseResults(output) {
const results = JSON.parse(output);

return results.reduce((current, result) => {
let time = {};

if (result.loading !== undefined && result.running !== undefined) {
const loading = +result.loading;
const running = +result.running;

time = {
running,
loading,
total: loading + running,
};
}

return {
// biome-ignore lint/performance/noAccumulatingSpread: <explanation>
...current,
[result.name]: {
name: result.name,
size: +result.size,
...time,
},
};
}, {});
}

hasSizeChanges(base, current, threshold = 0) {
const names = [...new Set([...(base ? Object.keys(base) : []), ...Object.keys(current)])];
const isSize = names.some(name => current[name] && current[name].total === undefined);

// Always return true if time results are present
if (!isSize) {
return true;
}

return !!names.find(name => {
const baseResult = base?.[name] || EmptyResult;
Expand All@@ -132,16 +122,12 @@ class SizeLimit {

formatResults(base, current) {
const names = [...new Set([...(base ? Object.keys(base) : []), ...Object.keys(current)])];
const isSize = names.some(name => current[name] && current[name].total === undefined);
const header = isSize ? SIZE_RESULTS_HEADER : TIME_RESULTS_HEADER;
const header = SIZE_RESULTS_HEADER;
const fields = names.map(name => {
const baseResult = base?.[name] || EmptyResult;
const currentResult = current[name] || EmptyResult;

if (isSize) {
return this.formatSizeResult(name, baseResult, currentResult);
}
return this.formatTimeResult(name, baseResult, currentResult);
return this.formatSizeResult(name, baseResult, currentResult);
});

return [header, ...fields];
Expand All@@ -165,15 +151,11 @@ async function execSizeLimit() {
return { status, output };
}

const SIZE_RESULTS_HEADER = ['Path', 'Size'];
const TIME_RESULTS_HEADER = ['Path', 'Size', 'Loading time (3g)', 'Running time (snapdragon)', 'Total time'];
const SIZE_RESULTS_HEADER = ['Path', 'Size', '% Change', 'Change'];

const EmptyResult = {
name: '-',
size: 0,
running: 0,
loading: 0,
total: 0,
};

async function run() {
Expand DownExpand Up@@ -227,6 +209,8 @@ async function run() {
// Else, we run size limit for the current branch, AND fetch it for the comparison branch
let base;
let current;
let baseIsNotLatest = false;
let baseWorkflowRun;

try {
const artifacts = await getArtifactsForBranchAndWorkflow(octokit, {
Expand All@@ -240,6 +224,8 @@ async function run() {
throw new Error('No artifacts found');
}

baseWorkflowRun = artifacts.workflowRun;

await downloadOtherWorkflowArtifact(octokit, {
...repo,
artifactName: ARTIFACT_NAME,
Expand All@@ -248,6 +234,11 @@ async function run() {
});

base = JSON.parse(await fs.readFile(resultsFilePath, { encoding: 'utf8' }));

if (!artifacts.isLatest) {
baseIsNotLatest = true;
core.info('Base artifact is not the latest one. This may lead to incorrect results.');
Comment thread
mydea marked this conversation as resolved.
}
} catch (error) {
core.startGroup('Warning, unable to find base results');
core.error(error);
Expand All@@ -271,7 +262,22 @@ async function run() {
isNaN(thresholdNumber) || limit.hasSizeChanges(base, current, thresholdNumber) || sizeLimitComment;

if (shouldComment) {
const body = [SIZE_LIMIT_HEADING, markdownTable(limit.formatResults(base, current))].join('\r\n');
const bodyParts = [SIZE_LIMIT_HEADING];

if (baseIsNotLatest) {
bodyParts.push(
'⚠️ **Warning:** Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.',
);
}

bodyParts.push(markdownTable(limit.formatResults(base, current)));

if (baseWorkflowRun) {
bodyParts.push('');
bodyParts.push(`[View base workflow run](${baseWorkflowRun.html_url})`);
}

const body = bodyParts.join('\r\n');

try {
if (!sizeLimitComment) {
Expand DownExpand Up@@ -320,7 +326,7 @@ const DEFAULT_PAGE_LIMIT = 10;
* This is a bit hacky since GitHub Actions currently does not directly
* support downloading artifacts from other workflows
*/
export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, workflowName, branch, artifactName }) {
async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, workflowName, branch, artifactName }) {
core.startGroup(`getArtifactsForBranchAndWorkflow - workflow:"${workflowName}", branch:"${branch}"`);

let repositoryWorkflow = null;
Expand DownExpand Up@@ -361,14 +367,13 @@ export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, w
const workflow_id = repositoryWorkflow.id;

let currentPage = 0;
const completedWorkflowRuns = [];
let latestWorkflowRun = null;

for await (const response of octokit.paginate.iterator(octokit.rest.actions.listWorkflowRuns, {
owner,
repo,
workflow_id,
branch,
status: 'completed',
per_page: DEFAULT_PAGE_LIMIT,
event: 'push',
})) {
Expand All@@ -379,12 +384,47 @@ export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, w
}

// Do not allow downloading artifacts from a fork.
completedWorkflowRuns.push(
...response.data.filter(workflowRun => workflowRun.head_repository.full_name === `${owner}/${repo}`),
);
const filtered = response.data.filter(workflowRun => workflowRun.head_repository.full_name === `${owner}/${repo}`);

// Sort to ensure the latest workflow run is the first
filtered.sort((a, b) => {
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
});

// Store the first workflow run, to determine if this is the latest one...
if (!latestWorkflowRun) {
latestWorkflowRun = filtered[0];
}

// Search through workflow artifacts until we find a workflow run w/ artifact name that we are looking for
for (const workflowRun of filtered) {
core.info(`Checking artifacts for workflow run: ${workflowRun.html_url}`);

if (completedWorkflowRuns.length) {
break;
const {
data: { artifacts },
} = await octokit.rest.actions.listWorkflowRunArtifacts({
owner,
repo,
run_id: workflowRun.id,
});

if (!artifacts) {
core.warning(
`Unable to fetch artifacts for branch: ${branch}, workflow: ${workflow_id}, workflowRunId: ${workflowRun.id}`,
);
} else {
const foundArtifact = artifacts.find(({ name }) => name === artifactName);
if (foundArtifact) {
core.info(`Found suitable artifact: ${foundArtifact.url}`);
return {
artifact: foundArtifact,
workflowRun,
isLatest: latestWorkflowRun.id === workflowRun.id,
};
} else {
core.info(`No artifact found for ${artifactName}, trying next workflow run...`);
}
}
}

if (currentPage > DEFAULT_MAX_PAGES) {
Expand All@@ -396,34 +436,6 @@ export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, w
currentPage++;
}

// Search through workflow artifacts until we find a workflow run w/ artifact name that we are looking for
for (const workflowRun of completedWorkflowRuns) {
core.info(`Checking artifacts for workflow run: ${workflowRun.html_url}`);

const {
data: { artifacts },
} = await octokit.rest.actions.listWorkflowRunArtifacts({
owner,
repo,
run_id: workflowRun.id,
});

if (!artifacts) {
core.warning(
`Unable to fetch artifacts for branch: ${branch}, workflow: ${workflow_id}, workflowRunId: ${workflowRun.id}`,
);
} else {
const foundArtifact = artifacts.find(({ name }) => name === artifactName);
if (foundArtifact) {
core.info(`Found suitable artifact: ${foundArtifact.url}`);
return {
artifact: foundArtifact,
workflowRun,
};
}
}
}

core.warning(`Artifact not found: ${artifactName}`);
core.endGroup();
return null;
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
182 changes: 97 additions & 85 deletions dev-packages/size-limit-gh-action/index.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,12 +31,27 @@ class SizeLimit {
return bytes.format(size, { unitSeparator: ' ' });
}

formatTime(seconds) {
if (seconds >= 1) {
return `${Math.ceil(seconds * 10) / 10} s`;
formatPercentageChange(base = 0, current = 0) {
if (base === 0) {
return 'added';
}

if (current === 0) {
return 'removed';
}

const value = ((current - base) / base) * 100;
const formatted = (Math.sign(value) * Math.ceil(Math.abs(value) * 100)) / 100;

if (value > 0) {
return `+${formatted}%`;
}

if (value === 0) {
return '-';
}

return `${Math.ceil(seconds * 1000)} ms`;
return `${formatted}%`;
}

formatChange(base = 0, current = 0) {
Expand All@@ -48,75 +63,50 @@ class SizeLimit {
return 'removed';
}

const value = ((current - base) / base) * 100;
const formatted = (Math.sign(value) * Math.ceil(Math.abs(value) * 100)) / 100;
const value = current - base;
const formatted = this.formatBytes(value);

if (value > 0) {
return `+${formatted}% 🔺`;
return `+${formatted} 🔺`;
}

if (value === 0) {
return `${formatted}%`;
return '-';
}

return `${formatted}% 🔽`;
return `${formatted} 🔽`;
}

formatLine(value, change) {
return `${value} (${change})`;
}

formatSizeResult(name, base, current) {
return [name, this.formatLine(this.formatBytes(current.size), this.formatChange(base.size, current.size))];
}

formatTimeResult(name, base, current) {
return [
name,
this.formatLine(this.formatBytes(current.size), this.formatChange(base.size, current.size)),
this.formatLine(this.formatTime(current.loading), this.formatChange(base.loading, current.loading)),
this.formatLine(this.formatTime(current.running), this.formatChange(base.running, current.running)),
this.formatTime(current.total),
this.formatBytes(current.size),
this.formatPercentageChange(base.size, current.size),
this.formatChange(base.size, current.size),
];
}

parseResults(output) {
const results = JSON.parse(output);

return results.reduce((current, result) => {
let time = {};

if (result.loading !== undefined && result.running !== undefined) {
const loading = +result.loading;
const running = +result.running;

time = {
running,
loading,
total: loading + running,
};
}

return {
// biome-ignore lint/performance/noAccumulatingSpread: <explanation>
...current,
[result.name]: {
name: result.name,
size: +result.size,
...time,
},
};
}, {});
}

hasSizeChanges(base, current, threshold = 0) {
const names = [...new Set([...(base ? Object.keys(base) : []), ...Object.keys(current)])];
const isSize = names.some(name => current[name] && current[name].total === undefined);

// Always return true if time results are present
if (!isSize) {
return true;
}

return !!names.find(name => {
const baseResult = base?.[name] || EmptyResult;
Expand All@@ -132,16 +122,12 @@ class SizeLimit {

formatResults(base, current) {
const names = [...new Set([...(base ? Object.keys(base) : []), ...Object.keys(current)])];
const isSize = names.some(name => current[name] && current[name].total === undefined);
const header = isSize ? SIZE_RESULTS_HEADER : TIME_RESULTS_HEADER;
const header = SIZE_RESULTS_HEADER;
const fields = names.map(name => {
const baseResult = base?.[name] || EmptyResult;
const currentResult = current[name] || EmptyResult;

if (isSize) {
return this.formatSizeResult(name, baseResult, currentResult);
}
return this.formatTimeResult(name, baseResult, currentResult);
return this.formatSizeResult(name, baseResult, currentResult);
});

return [header, ...fields];
Expand All@@ -165,15 +151,11 @@ async function execSizeLimit() {
return { status, output };
}

const SIZE_RESULTS_HEADER = ['Path', 'Size'];
const TIME_RESULTS_HEADER = ['Path', 'Size', 'Loading time (3g)', 'Running time (snapdragon)', 'Total time'];
const SIZE_RESULTS_HEADER = ['Path', 'Size', '% Change', 'Change'];

const EmptyResult = {
name: '-',
size: 0,
running: 0,
loading: 0,
total: 0,
};

async function run() {
Expand DownExpand Up@@ -227,6 +209,8 @@ async function run() {
// Else, we run size limit for the current branch, AND fetch it for the comparison branch
let base;
let current;
let baseIsNotLatest = false;
let baseWorkflowRun;

try {
const artifacts = await getArtifactsForBranchAndWorkflow(octokit, {
Expand All@@ -240,6 +224,8 @@ async function run() {
throw new Error('No artifacts found');
}

baseWorkflowRun = artifacts.workflowRun;

await downloadOtherWorkflowArtifact(octokit, {
...repo,
artifactName: ARTIFACT_NAME,
Expand All@@ -248,6 +234,11 @@ async function run() {
});

base = JSON.parse(await fs.readFile(resultsFilePath, { encoding: 'utf8' }));

if (!artifacts.isLatest) {
baseIsNotLatest = true;
core.info('Base artifact is not the latest one. This may lead to incorrect results.');
Comment thread
mydea marked this conversation as resolved.
}
} catch (error) {
core.startGroup('Warning, unable to find base results');
core.error(error);
Expand All@@ -271,7 +262,22 @@ async function run() {
isNaN(thresholdNumber) || limit.hasSizeChanges(base, current, thresholdNumber) || sizeLimitComment;

if (shouldComment) {
const body = [SIZE_LIMIT_HEADING, markdownTable(limit.formatResults(base, current))].join('\r\n');
const bodyParts = [SIZE_LIMIT_HEADING];

if (baseIsNotLatest) {
bodyParts.push(
'⚠️ **Warning:** Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.',
);
}

bodyParts.push(markdownTable(limit.formatResults(base, current)));

if (baseWorkflowRun) {
bodyParts.push('');
bodyParts.push(`[View base workflow run](${baseWorkflowRun.html_url})`);
}

const body = bodyParts.join('\r\n');

try {
if (!sizeLimitComment) {
Expand DownExpand Up@@ -320,7 +326,7 @@ const DEFAULT_PAGE_LIMIT = 10;
* This is a bit hacky since GitHub Actions currently does not directly
* support downloading artifacts from other workflows
*/
export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, workflowName, branch, artifactName }) {
async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, workflowName, branch, artifactName }) {
core.startGroup(`getArtifactsForBranchAndWorkflow - workflow:"${workflowName}", branch:"${branch}"`);

let repositoryWorkflow = null;
Expand DownExpand Up@@ -361,14 +367,13 @@ export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, w
const workflow_id = repositoryWorkflow.id;

let currentPage = 0;
const completedWorkflowRuns = [];
let latestWorkflowRun = null;

for await (const response of octokit.paginate.iterator(octokit.rest.actions.listWorkflowRuns, {
owner,
repo,
workflow_id,
branch,
status: 'completed',
per_page: DEFAULT_PAGE_LIMIT,
event: 'push',
})) {
Expand All@@ -379,12 +384,47 @@ export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, w
}

// Do not allow downloading artifacts from a fork.
completedWorkflowRuns.push(
...response.data.filter(workflowRun => workflowRun.head_repository.full_name === `${owner}/${repo}`),
);
const filtered = response.data.filter(workflowRun => workflowRun.head_repository.full_name === `${owner}/${repo}`);

// Sort to ensure the latest workflow run is the first
filtered.sort((a, b) => {
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
});

// Store the first workflow run, to determine if this is the latest one...
if (!latestWorkflowRun) {
latestWorkflowRun = filtered[0];
}

// Search through workflow artifacts until we find a workflow run w/ artifact name that we are looking for
for (const workflowRun of filtered) {
core.info(`Checking artifacts for workflow run: ${workflowRun.html_url}`);

if (completedWorkflowRuns.length) {
break;
const {
data: { artifacts },
} = await octokit.rest.actions.listWorkflowRunArtifacts({
owner,
repo,
run_id: workflowRun.id,
});

if (!artifacts) {
core.warning(
`Unable to fetch artifacts for branch: ${branch}, workflow: ${workflow_id}, workflowRunId: ${workflowRun.id}`,
);
} else {
const foundArtifact = artifacts.find(({ name }) => name === artifactName);
if (foundArtifact) {
core.info(`Found suitable artifact: ${foundArtifact.url}`);
return {
artifact: foundArtifact,
workflowRun,
isLatest: latestWorkflowRun.id === workflowRun.id,
};
} else {
core.info(`No artifact found for ${artifactName}, trying next workflow run...`);
}
}
}

if (currentPage > DEFAULT_MAX_PAGES) {
Expand All@@ -396,34 +436,6 @@ export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, w
currentPage++;
}

// Search through workflow artifacts until we find a workflow run w/ artifact name that we are looking for
for (const workflowRun of completedWorkflowRuns) {
core.info(`Checking artifacts for workflow run: ${workflowRun.html_url}`);

const {
data: { artifacts },
} = await octokit.rest.actions.listWorkflowRunArtifacts({
owner,
repo,
run_id: workflowRun.id,
});

if (!artifacts) {
core.warning(
`Unable to fetch artifacts for branch: ${branch}, workflow: ${workflow_id}, workflowRunId: ${workflowRun.id}`,
);
} else {
const foundArtifact = artifacts.find(({ name }) => name === artifactName);
if (foundArtifact) {
core.info(`Found suitable artifact: ${foundArtifact.url}`);
return {
artifact: foundArtifact,
workflowRun,
};
}
}
}

core.warning(`Artifact not found: ${artifactName}`);
core.endGroup();
return null;
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
182 changes: 97 additions & 85 deletions dev-packages/size-limit-gh-action/index.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,12 +31,27 @@ class SizeLimit {
return bytes.format(size, { unitSeparator: ' ' });
}

formatTime(seconds) {
if (seconds >= 1) {
return `${Math.ceil(seconds * 10) / 10} s`;
formatPercentageChange(base = 0, current = 0) {
if (base === 0) {
return 'added';
}

if (current === 0) {
return 'removed';
}

const value = ((current - base) / base) * 100;
const formatted = (Math.sign(value) * Math.ceil(Math.abs(value) * 100)) / 100;

if (value > 0) {
return `+${formatted}%`;
}

if (value === 0) {
return '-';
}

return `${Math.ceil(seconds * 1000)} ms`;
return `${formatted}%`;
}

formatChange(base = 0, current = 0) {
Expand All@@ -48,75 +63,50 @@ class SizeLimit {
return 'removed';
}

const value = ((current - base) / base) * 100;
const formatted = (Math.sign(value) * Math.ceil(Math.abs(value) * 100)) / 100;
const value = current - base;
const formatted = this.formatBytes(value);

if (value > 0) {
return `+${formatted}% 🔺`;
return `+${formatted} 🔺`;
}

if (value === 0) {
return `${formatted}%`;
return '-';
}

return `${formatted}% 🔽`;
return `${formatted} 🔽`;
}

formatLine(value, change) {
return `${value} (${change})`;
}

formatSizeResult(name, base, current) {
return [name, this.formatLine(this.formatBytes(current.size), this.formatChange(base.size, current.size))];
}

formatTimeResult(name, base, current) {
return [
name,
this.formatLine(this.formatBytes(current.size), this.formatChange(base.size, current.size)),
this.formatLine(this.formatTime(current.loading), this.formatChange(base.loading, current.loading)),
this.formatLine(this.formatTime(current.running), this.formatChange(base.running, current.running)),
this.formatTime(current.total),
this.formatBytes(current.size),
this.formatPercentageChange(base.size, current.size),
this.formatChange(base.size, current.size),
];
}

parseResults(output) {
const results = JSON.parse(output);

return results.reduce((current, result) => {
let time = {};

if (result.loading !== undefined && result.running !== undefined) {
const loading = +result.loading;
const running = +result.running;

time = {
running,
loading,
total: loading + running,
};
}

return {
// biome-ignore lint/performance/noAccumulatingSpread: <explanation>
...current,
[result.name]: {
name: result.name,
size: +result.size,
...time,
},
};
}, {});
}

hasSizeChanges(base, current, threshold = 0) {
const names = [...new Set([...(base ? Object.keys(base) : []), ...Object.keys(current)])];
const isSize = names.some(name => current[name] && current[name].total === undefined);

// Always return true if time results are present
if (!isSize) {
return true;
}

return !!names.find(name => {
const baseResult = base?.[name] || EmptyResult;
Expand All@@ -132,16 +122,12 @@ class SizeLimit {

formatResults(base, current) {
const names = [...new Set([...(base ? Object.keys(base) : []), ...Object.keys(current)])];
const isSize = names.some(name => current[name] && current[name].total === undefined);
const header = isSize ? SIZE_RESULTS_HEADER : TIME_RESULTS_HEADER;
const header = SIZE_RESULTS_HEADER;
const fields = names.map(name => {
const baseResult = base?.[name] || EmptyResult;
const currentResult = current[name] || EmptyResult;

if (isSize) {
return this.formatSizeResult(name, baseResult, currentResult);
}
return this.formatTimeResult(name, baseResult, currentResult);
return this.formatSizeResult(name, baseResult, currentResult);
});

return [header, ...fields];
Expand All@@ -165,15 +151,11 @@ async function execSizeLimit() {
return { status, output };
}

const SIZE_RESULTS_HEADER = ['Path', 'Size'];
const TIME_RESULTS_HEADER = ['Path', 'Size', 'Loading time (3g)', 'Running time (snapdragon)', 'Total time'];
const SIZE_RESULTS_HEADER = ['Path', 'Size', '% Change', 'Change'];

const EmptyResult = {
name: '-',
size: 0,
running: 0,
loading: 0,
total: 0,
};

async function run() {
Expand DownExpand Up@@ -227,6 +209,8 @@ async function run() {
// Else, we run size limit for the current branch, AND fetch it for the comparison branch
let base;
let current;
let baseIsNotLatest = false;
let baseWorkflowRun;

try {
const artifacts = await getArtifactsForBranchAndWorkflow(octokit, {
Expand All@@ -240,6 +224,8 @@ async function run() {
throw new Error('No artifacts found');
}

baseWorkflowRun = artifacts.workflowRun;

await downloadOtherWorkflowArtifact(octokit, {
...repo,
artifactName: ARTIFACT_NAME,
Expand All@@ -248,6 +234,11 @@ async function run() {
});

base = JSON.parse(await fs.readFile(resultsFilePath, { encoding: 'utf8' }));

if (!artifacts.isLatest) {
baseIsNotLatest = true;
core.info('Base artifact is not the latest one. This may lead to incorrect results.');
Comment thread
mydea marked this conversation as resolved.
}
} catch (error) {
core.startGroup('Warning, unable to find base results');
core.error(error);
Expand All@@ -271,7 +262,22 @@ async function run() {
isNaN(thresholdNumber) || limit.hasSizeChanges(base, current, thresholdNumber) || sizeLimitComment;

if (shouldComment) {
const body = [SIZE_LIMIT_HEADING, markdownTable(limit.formatResults(base, current))].join('\r\n');
const bodyParts = [SIZE_LIMIT_HEADING];

if (baseIsNotLatest) {
bodyParts.push(
'⚠️ **Warning:** Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.',
);
}

bodyParts.push(markdownTable(limit.formatResults(base, current)));

if (baseWorkflowRun) {
bodyParts.push('');
bodyParts.push(`[View base workflow run](${baseWorkflowRun.html_url})`);
}

const body = bodyParts.join('\r\n');

try {
if (!sizeLimitComment) {
Expand DownExpand Up@@ -320,7 +326,7 @@ const DEFAULT_PAGE_LIMIT = 10;
* This is a bit hacky since GitHub Actions currently does not directly
* support downloading artifacts from other workflows
*/
export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, workflowName, branch, artifactName }) {
async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, workflowName, branch, artifactName }) {
core.startGroup(`getArtifactsForBranchAndWorkflow - workflow:"${workflowName}", branch:"${branch}"`);

let repositoryWorkflow = null;
Expand DownExpand Up@@ -361,14 +367,13 @@ export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, w
const workflow_id = repositoryWorkflow.id;

let currentPage = 0;
const completedWorkflowRuns = [];
let latestWorkflowRun = null;

for await (const response of octokit.paginate.iterator(octokit.rest.actions.listWorkflowRuns, {
owner,
repo,
workflow_id,
branch,
status: 'completed',
per_page: DEFAULT_PAGE_LIMIT,
event: 'push',
})) {
Expand All@@ -379,12 +384,47 @@ export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, w
}

// Do not allow downloading artifacts from a fork.
completedWorkflowRuns.push(
...response.data.filter(workflowRun => workflowRun.head_repository.full_name === `${owner}/${repo}`),
);
const filtered = response.data.filter(workflowRun => workflowRun.head_repository.full_name === `${owner}/${repo}`);

// Sort to ensure the latest workflow run is the first
filtered.sort((a, b) => {
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
});

// Store the first workflow run, to determine if this is the latest one...
if (!latestWorkflowRun) {
latestWorkflowRun = filtered[0];
}

// Search through workflow artifacts until we find a workflow run w/ artifact name that we are looking for
for (const workflowRun of filtered) {
core.info(`Checking artifacts for workflow run: ${workflowRun.html_url}`);

if (completedWorkflowRuns.length) {
break;
const {
data: { artifacts },
} = await octokit.rest.actions.listWorkflowRunArtifacts({
owner,
repo,
run_id: workflowRun.id,
});

if (!artifacts) {
core.warning(
`Unable to fetch artifacts for branch: ${branch}, workflow: ${workflow_id}, workflowRunId: ${workflowRun.id}`,
);
} else {
const foundArtifact = artifacts.find(({ name }) => name === artifactName);
if (foundArtifact) {
core.info(`Found suitable artifact: ${foundArtifact.url}`);
return {
artifact: foundArtifact,
workflowRun,
isLatest: latestWorkflowRun.id === workflowRun.id,
};
} else {
core.info(`No artifact found for ${artifactName}, trying next workflow run...`);
}
}
}

if (currentPage > DEFAULT_MAX_PAGES) {
Expand All@@ -396,34 +436,6 @@ export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, w
currentPage++;
}

// Search through workflow artifacts until we find a workflow run w/ artifact name that we are looking for
for (const workflowRun of completedWorkflowRuns) {
core.info(`Checking artifacts for workflow run: ${workflowRun.html_url}`);

const {
data: { artifacts },
} = await octokit.rest.actions.listWorkflowRunArtifacts({
owner,
repo,
run_id: workflowRun.id,
});

if (!artifacts) {
core.warning(
`Unable to fetch artifacts for branch: ${branch}, workflow: ${workflow_id}, workflowRunId: ${workflowRun.id}`,
);
} else {
const foundArtifact = artifacts.find(({ name }) => name === artifactName);
if (foundArtifact) {
core.info(`Found suitable artifact: ${foundArtifact.url}`);
return {
artifact: foundArtifact,
workflowRun,
};
}
}
}

core.warning(`Artifact not found: ${artifactName}`);
core.endGroup();
return null;
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
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
182 changes: 97 additions & 85 deletions dev-packages/size-limit-gh-action/index.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,12 +31,27 @@ class SizeLimit {
return bytes.format(size, { unitSeparator: ' ' });
}

formatTime(seconds) {
if (seconds >= 1) {
return `${Math.ceil(seconds * 10) / 10} s`;
formatPercentageChange(base = 0, current = 0) {
if (base === 0) {
return 'added';
}

if (current === 0) {
return 'removed';
}

const value = ((current - base) / base) * 100;
const formatted = (Math.sign(value) * Math.ceil(Math.abs(value) * 100)) / 100;

if (value > 0) {
return `+${formatted}%`;
}

if (value === 0) {
return '-';
}

return `${Math.ceil(seconds * 1000)} ms`;
return `${formatted}%`;
}

formatChange(base = 0, current = 0) {
Expand All@@ -48,75 +63,50 @@ class SizeLimit {
return 'removed';
}

const value = ((current - base) / base) * 100;
const formatted = (Math.sign(value) * Math.ceil(Math.abs(value) * 100)) / 100;
const value = current - base;
const formatted = this.formatBytes(value);

if (value > 0) {
return `+${formatted}% 🔺`;
return `+${formatted} 🔺`;
}

if (value === 0) {
return `${formatted}%`;
return '-';
}

return `${formatted}% 🔽`;
return `${formatted} 🔽`;
}

formatLine(value, change) {
return `${value} (${change})`;
}

formatSizeResult(name, base, current) {
return [name, this.formatLine(this.formatBytes(current.size), this.formatChange(base.size, current.size))];
}

formatTimeResult(name, base, current) {
return [
name,
this.formatLine(this.formatBytes(current.size), this.formatChange(base.size, current.size)),
this.formatLine(this.formatTime(current.loading), this.formatChange(base.loading, current.loading)),
this.formatLine(this.formatTime(current.running), this.formatChange(base.running, current.running)),
this.formatTime(current.total),
this.formatBytes(current.size),
this.formatPercentageChange(base.size, current.size),
this.formatChange(base.size, current.size),
];
}

parseResults(output) {
const results = JSON.parse(output);

return results.reduce((current, result) => {
let time = {};

if (result.loading !== undefined && result.running !== undefined) {
const loading = +result.loading;
const running = +result.running;

time = {
running,
loading,
total: loading + running,
};
}

return {
// biome-ignore lint/performance/noAccumulatingSpread: <explanation>
...current,
[result.name]: {
name: result.name,
size: +result.size,
...time,
},
};
}, {});
}

hasSizeChanges(base, current, threshold = 0) {
const names = [...new Set([...(base ? Object.keys(base) : []), ...Object.keys(current)])];
const isSize = names.some(name => current[name] && current[name].total === undefined);

// Always return true if time results are present
if (!isSize) {
return true;
}

return !!names.find(name => {
const baseResult = base?.[name] || EmptyResult;
Expand All@@ -132,16 +122,12 @@ class SizeLimit {

formatResults(base, current) {
const names = [...new Set([...(base ? Object.keys(base) : []), ...Object.keys(current)])];
const isSize = names.some(name => current[name] && current[name].total === undefined);
const header = isSize ? SIZE_RESULTS_HEADER : TIME_RESULTS_HEADER;
const header = SIZE_RESULTS_HEADER;
const fields = names.map(name => {
const baseResult = base?.[name] || EmptyResult;
const currentResult = current[name] || EmptyResult;

if (isSize) {
return this.formatSizeResult(name, baseResult, currentResult);
}
return this.formatTimeResult(name, baseResult, currentResult);
return this.formatSizeResult(name, baseResult, currentResult);
});

return [header, ...fields];
Expand All@@ -165,15 +151,11 @@ async function execSizeLimit() {
return { status, output };
}

const SIZE_RESULTS_HEADER = ['Path', 'Size'];
const TIME_RESULTS_HEADER = ['Path', 'Size', 'Loading time (3g)', 'Running time (snapdragon)', 'Total time'];
const SIZE_RESULTS_HEADER = ['Path', 'Size', '% Change', 'Change'];

const EmptyResult = {
name: '-',
size: 0,
running: 0,
loading: 0,
total: 0,
};

async function run() {
Expand DownExpand Up@@ -227,6 +209,8 @@ async function run() {
// Else, we run size limit for the current branch, AND fetch it for the comparison branch
let base;
let current;
let baseIsNotLatest = false;
let baseWorkflowRun;

try {
const artifacts = await getArtifactsForBranchAndWorkflow(octokit, {
Expand All@@ -240,6 +224,8 @@ async function run() {
throw new Error('No artifacts found');
}

baseWorkflowRun = artifacts.workflowRun;

await downloadOtherWorkflowArtifact(octokit, {
...repo,
artifactName: ARTIFACT_NAME,
Expand All@@ -248,6 +234,11 @@ async function run() {
});

base = JSON.parse(await fs.readFile(resultsFilePath, { encoding: 'utf8' }));

if (!artifacts.isLatest) {
baseIsNotLatest = true;
core.info('Base artifact is not the latest one. This may lead to incorrect results.');
Comment thread
mydea marked this conversation as resolved.
}
} catch (error) {
core.startGroup('Warning, unable to find base results');
core.error(error);
Expand All@@ -271,7 +262,22 @@ async function run() {
isNaN(thresholdNumber) || limit.hasSizeChanges(base, current, thresholdNumber) || sizeLimitComment;

if (shouldComment) {
const body = [SIZE_LIMIT_HEADING, markdownTable(limit.formatResults(base, current))].join('\r\n');
const bodyParts = [SIZE_LIMIT_HEADING];

if (baseIsNotLatest) {
bodyParts.push(
'⚠️ **Warning:** Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.',
);
}

bodyParts.push(markdownTable(limit.formatResults(base, current)));

if (baseWorkflowRun) {
bodyParts.push('');
bodyParts.push(`[View base workflow run](${baseWorkflowRun.html_url})`);
}

const body = bodyParts.join('\r\n');

try {
if (!sizeLimitComment) {
Expand DownExpand Up@@ -320,7 +326,7 @@ const DEFAULT_PAGE_LIMIT = 10;
* This is a bit hacky since GitHub Actions currently does not directly
* support downloading artifacts from other workflows
*/
export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, workflowName, branch, artifactName }) {
async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, workflowName, branch, artifactName }) {
core.startGroup(`getArtifactsForBranchAndWorkflow - workflow:"${workflowName}", branch:"${branch}"`);

let repositoryWorkflow = null;
Expand DownExpand Up@@ -361,14 +367,13 @@ export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, w
const workflow_id = repositoryWorkflow.id;

let currentPage = 0;
const completedWorkflowRuns = [];
let latestWorkflowRun = null;

for await (const response of octokit.paginate.iterator(octokit.rest.actions.listWorkflowRuns, {
owner,
repo,
workflow_id,
branch,
status: 'completed',
per_page: DEFAULT_PAGE_LIMIT,
event: 'push',
})) {
Expand All@@ -379,12 +384,47 @@ export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, w
}

// Do not allow downloading artifacts from a fork.
completedWorkflowRuns.push(
...response.data.filter(workflowRun => workflowRun.head_repository.full_name === `${owner}/${repo}`),
);
const filtered = response.data.filter(workflowRun => workflowRun.head_repository.full_name === `${owner}/${repo}`);

// Sort to ensure the latest workflow run is the first
filtered.sort((a, b) => {
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
});

// Store the first workflow run, to determine if this is the latest one...
if (!latestWorkflowRun) {
latestWorkflowRun = filtered[0];
}

// Search through workflow artifacts until we find a workflow run w/ artifact name that we are looking for
for (const workflowRun of filtered) {
core.info(`Checking artifacts for workflow run: ${workflowRun.html_url}`);

if (completedWorkflowRuns.length) {
break;
const {
data: { artifacts },
} = await octokit.rest.actions.listWorkflowRunArtifacts({
owner,
repo,
run_id: workflowRun.id,
});

if (!artifacts) {
core.warning(
`Unable to fetch artifacts for branch: ${branch}, workflow: ${workflow_id}, workflowRunId: ${workflowRun.id}`,
);
} else {
const foundArtifact = artifacts.find(({ name }) => name === artifactName);
if (foundArtifact) {
core.info(`Found suitable artifact: ${foundArtifact.url}`);
return {
artifact: foundArtifact,
workflowRun,
isLatest: latestWorkflowRun.id === workflowRun.id,
};
} else {
core.info(`No artifact found for ${artifactName}, trying next workflow run...`);
}
}
}

if (currentPage > DEFAULT_MAX_PAGES) {
Expand All@@ -396,34 +436,6 @@ export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, w
currentPage++;
}

// Search through workflow artifacts until we find a workflow run w/ artifact name that we are looking for
for (const workflowRun of completedWorkflowRuns) {
core.info(`Checking artifacts for workflow run: ${workflowRun.html_url}`);

const {
data: { artifacts },
} = await octokit.rest.actions.listWorkflowRunArtifacts({
owner,
repo,
run_id: workflowRun.id,
});

if (!artifacts) {
core.warning(
`Unable to fetch artifacts for branch: ${branch}, workflow: ${workflow_id}, workflowRunId: ${workflowRun.id}`,
);
} else {
const foundArtifact = artifacts.find(({ name }) => name === artifactName);
if (foundArtifact) {
core.info(`Found suitable artifact: ${foundArtifact.url}`);
return {
artifact: foundArtifact,
workflowRun,
};
}
}
}

core.warning(`Artifact not found: ${artifactName}`);
core.endGroup();
return null;
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
182 changes: 97 additions & 85 deletions dev-packages/size-limit-gh-action/index.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,12 +31,27 @@ class SizeLimit {
return bytes.format(size, { unitSeparator: ' ' });
}

formatTime(seconds) {
if (seconds >= 1) {
return `${Math.ceil(seconds * 10) / 10} s`;
formatPercentageChange(base = 0, current = 0) {
if (base === 0) {
return 'added';
}

if (current === 0) {
return 'removed';
}

const value = ((current - base) / base) * 100;
const formatted = (Math.sign(value) * Math.ceil(Math.abs(value) * 100)) / 100;

if (value > 0) {
return `+${formatted}%`;
}

if (value === 0) {
return '-';
}

return `${Math.ceil(seconds * 1000)} ms`;
return `${formatted}%`;
}

formatChange(base = 0, current = 0) {
Expand All@@ -48,75 +63,50 @@ class SizeLimit {
return 'removed';
}

const value = ((current - base) / base) * 100;
const formatted = (Math.sign(value) * Math.ceil(Math.abs(value) * 100)) / 100;
const value = current - base;
const formatted = this.formatBytes(value);

if (value > 0) {
return `+${formatted}% 🔺`;
return `+${formatted} 🔺`;
}

if (value === 0) {
return `${formatted}%`;
return '-';
}

return `${formatted}% 🔽`;
return `${formatted} 🔽`;
}

formatLine(value, change) {
return `${value} (${change})`;
}

formatSizeResult(name, base, current) {
return [name, this.formatLine(this.formatBytes(current.size), this.formatChange(base.size, current.size))];
}

formatTimeResult(name, base, current) {
return [
name,
this.formatLine(this.formatBytes(current.size), this.formatChange(base.size, current.size)),
this.formatLine(this.formatTime(current.loading), this.formatChange(base.loading, current.loading)),
this.formatLine(this.formatTime(current.running), this.formatChange(base.running, current.running)),
this.formatTime(current.total),
this.formatBytes(current.size),
this.formatPercentageChange(base.size, current.size),
this.formatChange(base.size, current.size),
];
}

parseResults(output) {
const results = JSON.parse(output);

return results.reduce((current, result) => {
let time = {};

if (result.loading !== undefined && result.running !== undefined) {
const loading = +result.loading;
const running = +result.running;

time = {
running,
loading,
total: loading + running,
};
}

return {
// biome-ignore lint/performance/noAccumulatingSpread: <explanation>
...current,
[result.name]: {
name: result.name,
size: +result.size,
...time,
},
};
}, {});
}

hasSizeChanges(base, current, threshold = 0) {
const names = [...new Set([...(base ? Object.keys(base) : []), ...Object.keys(current)])];
const isSize = names.some(name => current[name] && current[name].total === undefined);

// Always return true if time results are present
if (!isSize) {
return true;
}

return !!names.find(name => {
const baseResult = base?.[name] || EmptyResult;
Expand All@@ -132,16 +122,12 @@ class SizeLimit {

formatResults(base, current) {
const names = [...new Set([...(base ? Object.keys(base) : []), ...Object.keys(current)])];
const isSize = names.some(name => current[name] && current[name].total === undefined);
const header = isSize ? SIZE_RESULTS_HEADER : TIME_RESULTS_HEADER;
const header = SIZE_RESULTS_HEADER;
const fields = names.map(name => {
const baseResult = base?.[name] || EmptyResult;
const currentResult = current[name] || EmptyResult;

if (isSize) {
return this.formatSizeResult(name, baseResult, currentResult);
}
return this.formatTimeResult(name, baseResult, currentResult);
return this.formatSizeResult(name, baseResult, currentResult);
});

return [header, ...fields];
Expand All@@ -165,15 +151,11 @@ async function execSizeLimit() {
return { status, output };
}

const SIZE_RESULTS_HEADER = ['Path', 'Size'];
const TIME_RESULTS_HEADER = ['Path', 'Size', 'Loading time (3g)', 'Running time (snapdragon)', 'Total time'];
const SIZE_RESULTS_HEADER = ['Path', 'Size', '% Change', 'Change'];

const EmptyResult = {
name: '-',
size: 0,
running: 0,
loading: 0,
total: 0,
};

async function run() {
Expand DownExpand Up@@ -227,6 +209,8 @@ async function run() {
// Else, we run size limit for the current branch, AND fetch it for the comparison branch
let base;
let current;
let baseIsNotLatest = false;
let baseWorkflowRun;

try {
const artifacts = await getArtifactsForBranchAndWorkflow(octokit, {
Expand All@@ -240,6 +224,8 @@ async function run() {
throw new Error('No artifacts found');
}

baseWorkflowRun = artifacts.workflowRun;

await downloadOtherWorkflowArtifact(octokit, {
...repo,
artifactName: ARTIFACT_NAME,
Expand All@@ -248,6 +234,11 @@ async function run() {
});

base = JSON.parse(await fs.readFile(resultsFilePath, { encoding: 'utf8' }));

if (!artifacts.isLatest) {
baseIsNotLatest = true;
core.info('Base artifact is not the latest one. This may lead to incorrect results.');
Comment thread
mydea marked this conversation as resolved.
}
} catch (error) {
core.startGroup('Warning, unable to find base results');
core.error(error);
Expand All@@ -271,7 +262,22 @@ async function run() {
isNaN(thresholdNumber) || limit.hasSizeChanges(base, current, thresholdNumber) || sizeLimitComment;

if (shouldComment) {
const body = [SIZE_LIMIT_HEADING, markdownTable(limit.formatResults(base, current))].join('\r\n');
const bodyParts = [SIZE_LIMIT_HEADING];

if (baseIsNotLatest) {
bodyParts.push(
'⚠️ **Warning:** Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.',
);
}

bodyParts.push(markdownTable(limit.formatResults(base, current)));

if (baseWorkflowRun) {
bodyParts.push('');
bodyParts.push(`[View base workflow run](${baseWorkflowRun.html_url})`);
}

const body = bodyParts.join('\r\n');

try {
if (!sizeLimitComment) {
Expand DownExpand Up@@ -320,7 +326,7 @@ const DEFAULT_PAGE_LIMIT = 10;
* This is a bit hacky since GitHub Actions currently does not directly
* support downloading artifacts from other workflows
*/
export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, workflowName, branch, artifactName }) {
async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, workflowName, branch, artifactName }) {
core.startGroup(`getArtifactsForBranchAndWorkflow - workflow:"${workflowName}", branch:"${branch}"`);

let repositoryWorkflow = null;
Expand DownExpand Up@@ -361,14 +367,13 @@ export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, w
const workflow_id = repositoryWorkflow.id;

let currentPage = 0;
const completedWorkflowRuns = [];
let latestWorkflowRun = null;

for await (const response of octokit.paginate.iterator(octokit.rest.actions.listWorkflowRuns, {
owner,
repo,
workflow_id,
branch,
status: 'completed',
per_page: DEFAULT_PAGE_LIMIT,
event: 'push',
})) {
Expand All@@ -379,12 +384,47 @@ export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, w
}

// Do not allow downloading artifacts from a fork.
completedWorkflowRuns.push(
...response.data.filter(workflowRun => workflowRun.head_repository.full_name === `${owner}/${repo}`),
);
const filtered = response.data.filter(workflowRun => workflowRun.head_repository.full_name === `${owner}/${repo}`);

// Sort to ensure the latest workflow run is the first
filtered.sort((a, b) => {
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
});

// Store the first workflow run, to determine if this is the latest one...
if (!latestWorkflowRun) {
latestWorkflowRun = filtered[0];
}

// Search through workflow artifacts until we find a workflow run w/ artifact name that we are looking for
for (const workflowRun of filtered) {
core.info(`Checking artifacts for workflow run: ${workflowRun.html_url}`);

if (completedWorkflowRuns.length) {
break;
const {
data: { artifacts },
} = await octokit.rest.actions.listWorkflowRunArtifacts({
owner,
repo,
run_id: workflowRun.id,
});

if (!artifacts) {
core.warning(
`Unable to fetch artifacts for branch: ${branch}, workflow: ${workflow_id}, workflowRunId: ${workflowRun.id}`,
);
} else {
const foundArtifact = artifacts.find(({ name }) => name === artifactName);
if (foundArtifact) {
core.info(`Found suitable artifact: ${foundArtifact.url}`);
return {
artifact: foundArtifact,
workflowRun,
isLatest: latestWorkflowRun.id === workflowRun.id,
};
} else {
core.info(`No artifact found for ${artifactName}, trying next workflow run...`);
}
}
}

if (currentPage > DEFAULT_MAX_PAGES) {
Expand All@@ -396,34 +436,6 @@ export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, w
currentPage++;
}

// Search through workflow artifacts until we find a workflow run w/ artifact name that we are looking for
for (const workflowRun of completedWorkflowRuns) {
core.info(`Checking artifacts for workflow run: ${workflowRun.html_url}`);

const {
data: { artifacts },
} = await octokit.rest.actions.listWorkflowRunArtifacts({
owner,
repo,
run_id: workflowRun.id,
});

if (!artifacts) {
core.warning(
`Unable to fetch artifacts for branch: ${branch}, workflow: ${workflow_id}, workflowRunId: ${workflowRun.id}`,
);
} else {
const foundArtifact = artifacts.find(({ name }) => name === artifactName);
if (foundArtifact) {
core.info(`Found suitable artifact: ${foundArtifact.url}`);
return {
artifact: foundArtifact,
workflowRun,
};
}
}
}

core.warning(`Artifact not found: ${artifactName}`);
core.endGroup();
return null;
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
182 changes: 97 additions & 85 deletions dev-packages/size-limit-gh-action/index.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,12 +31,27 @@ class SizeLimit {
return bytes.format(size, { unitSeparator: ' ' });
}

formatTime(seconds) {
if (seconds >= 1) {
return `${Math.ceil(seconds * 10) / 10} s`;
formatPercentageChange(base = 0, current = 0) {
if (base === 0) {
return 'added';
}

if (current === 0) {
return 'removed';
}

const value = ((current - base) / base) * 100;
const formatted = (Math.sign(value) * Math.ceil(Math.abs(value) * 100)) / 100;

if (value > 0) {
return `+${formatted}%`;
}

if (value === 0) {
return '-';
}

return `${Math.ceil(seconds * 1000)} ms`;
return `${formatted}%`;
}

formatChange(base = 0, current = 0) {
Expand All@@ -48,75 +63,50 @@ class SizeLimit {
return 'removed';
}

const value = ((current - base) / base) * 100;
const formatted = (Math.sign(value) * Math.ceil(Math.abs(value) * 100)) / 100;
const value = current - base;
const formatted = this.formatBytes(value);

if (value > 0) {
return `+${formatted}% 🔺`;
return `+${formatted} 🔺`;
}

if (value === 0) {
return `${formatted}%`;
return '-';
}

return `${formatted}% 🔽`;
return `${formatted} 🔽`;
}

formatLine(value, change) {
return `${value} (${change})`;
}

formatSizeResult(name, base, current) {
return [name, this.formatLine(this.formatBytes(current.size), this.formatChange(base.size, current.size))];
}

formatTimeResult(name, base, current) {
return [
name,
this.formatLine(this.formatBytes(current.size), this.formatChange(base.size, current.size)),
this.formatLine(this.formatTime(current.loading), this.formatChange(base.loading, current.loading)),
this.formatLine(this.formatTime(current.running), this.formatChange(base.running, current.running)),
this.formatTime(current.total),
this.formatBytes(current.size),
this.formatPercentageChange(base.size, current.size),
this.formatChange(base.size, current.size),
];
}

parseResults(output) {
const results = JSON.parse(output);

return results.reduce((current, result) => {
let time = {};

if (result.loading !== undefined && result.running !== undefined) {
const loading = +result.loading;
const running = +result.running;

time = {
running,
loading,
total: loading + running,
};
}

return {
// biome-ignore lint/performance/noAccumulatingSpread: <explanation>
...current,
[result.name]: {
name: result.name,
size: +result.size,
...time,
},
};
}, {});
}

hasSizeChanges(base, current, threshold = 0) {
const names = [...new Set([...(base ? Object.keys(base) : []), ...Object.keys(current)])];
const isSize = names.some(name => current[name] && current[name].total === undefined);

// Always return true if time results are present
if (!isSize) {
return true;
}

return !!names.find(name => {
const baseResult = base?.[name] || EmptyResult;
Expand All@@ -132,16 +122,12 @@ class SizeLimit {

formatResults(base, current) {
const names = [...new Set([...(base ? Object.keys(base) : []), ...Object.keys(current)])];
const isSize = names.some(name => current[name] && current[name].total === undefined);
const header = isSize ? SIZE_RESULTS_HEADER : TIME_RESULTS_HEADER;
const header = SIZE_RESULTS_HEADER;
const fields = names.map(name => {
const baseResult = base?.[name] || EmptyResult;
const currentResult = current[name] || EmptyResult;

if (isSize) {
return this.formatSizeResult(name, baseResult, currentResult);
}
return this.formatTimeResult(name, baseResult, currentResult);
return this.formatSizeResult(name, baseResult, currentResult);
});

return [header, ...fields];
Expand All@@ -165,15 +151,11 @@ async function execSizeLimit() {
return { status, output };
}

const SIZE_RESULTS_HEADER = ['Path', 'Size'];
const TIME_RESULTS_HEADER = ['Path', 'Size', 'Loading time (3g)', 'Running time (snapdragon)', 'Total time'];
const SIZE_RESULTS_HEADER = ['Path', 'Size', '% Change', 'Change'];

const EmptyResult = {
name: '-',
size: 0,
running: 0,
loading: 0,
total: 0,
};

async function run() {
Expand DownExpand Up@@ -227,6 +209,8 @@ async function run() {
// Else, we run size limit for the current branch, AND fetch it for the comparison branch
let base;
let current;
let baseIsNotLatest = false;
let baseWorkflowRun;

try {
const artifacts = await getArtifactsForBranchAndWorkflow(octokit, {
Expand All@@ -240,6 +224,8 @@ async function run() {
throw new Error('No artifacts found');
}

baseWorkflowRun = artifacts.workflowRun;

await downloadOtherWorkflowArtifact(octokit, {
...repo,
artifactName: ARTIFACT_NAME,
Expand All@@ -248,6 +234,11 @@ async function run() {
});

base = JSON.parse(await fs.readFile(resultsFilePath, { encoding: 'utf8' }));

if (!artifacts.isLatest) {
baseIsNotLatest = true;
core.info('Base artifact is not the latest one. This may lead to incorrect results.');
Comment thread
mydea marked this conversation as resolved.
}
} catch (error) {
core.startGroup('Warning, unable to find base results');
core.error(error);
Expand All@@ -271,7 +262,22 @@ async function run() {
isNaN(thresholdNumber) || limit.hasSizeChanges(base, current, thresholdNumber) || sizeLimitComment;

if (shouldComment) {
const body = [SIZE_LIMIT_HEADING, markdownTable(limit.formatResults(base, current))].join('\r\n');
const bodyParts = [SIZE_LIMIT_HEADING];

if (baseIsNotLatest) {
bodyParts.push(
'⚠️ **Warning:** Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.',
);
}

bodyParts.push(markdownTable(limit.formatResults(base, current)));

if (baseWorkflowRun) {
bodyParts.push('');
bodyParts.push(`[View base workflow run](${baseWorkflowRun.html_url})`);
}

const body = bodyParts.join('\r\n');

try {
if (!sizeLimitComment) {
Expand DownExpand Up@@ -320,7 +326,7 @@ const DEFAULT_PAGE_LIMIT = 10;
* This is a bit hacky since GitHub Actions currently does not directly
* support downloading artifacts from other workflows
*/
export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, workflowName, branch, artifactName }) {
async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, workflowName, branch, artifactName }) {
core.startGroup(`getArtifactsForBranchAndWorkflow - workflow:"${workflowName}", branch:"${branch}"`);

let repositoryWorkflow = null;
Expand DownExpand Up@@ -361,14 +367,13 @@ export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, w
const workflow_id = repositoryWorkflow.id;

let currentPage = 0;
const completedWorkflowRuns = [];
let latestWorkflowRun = null;

for await (const response of octokit.paginate.iterator(octokit.rest.actions.listWorkflowRuns, {
owner,
repo,
workflow_id,
branch,
status: 'completed',
per_page: DEFAULT_PAGE_LIMIT,
event: 'push',
})) {
Expand All@@ -379,12 +384,47 @@ export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, w
}

// Do not allow downloading artifacts from a fork.
completedWorkflowRuns.push(
...response.data.filter(workflowRun => workflowRun.head_repository.full_name === `${owner}/${repo}`),
);
const filtered = response.data.filter(workflowRun => workflowRun.head_repository.full_name === `${owner}/${repo}`);

// Sort to ensure the latest workflow run is the first
filtered.sort((a, b) => {
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
});

// Store the first workflow run, to determine if this is the latest one...
if (!latestWorkflowRun) {
latestWorkflowRun = filtered[0];
}

// Search through workflow artifacts until we find a workflow run w/ artifact name that we are looking for
for (const workflowRun of filtered) {
core.info(`Checking artifacts for workflow run: ${workflowRun.html_url}`);

if (completedWorkflowRuns.length) {
break;
const {
data: { artifacts },
} = await octokit.rest.actions.listWorkflowRunArtifacts({
owner,
repo,
run_id: workflowRun.id,
});

if (!artifacts) {
core.warning(
`Unable to fetch artifacts for branch: ${branch}, workflow: ${workflow_id}, workflowRunId: ${workflowRun.id}`,
);
} else {
const foundArtifact = artifacts.find(({ name }) => name === artifactName);
if (foundArtifact) {
core.info(`Found suitable artifact: ${foundArtifact.url}`);
return {
artifact: foundArtifact,
workflowRun,
isLatest: latestWorkflowRun.id === workflowRun.id,
};
} else {
core.info(`No artifact found for ${artifactName}, trying next workflow run...`);
}
}
}

if (currentPage > DEFAULT_MAX_PAGES) {
Expand All@@ -396,34 +436,6 @@ export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, w
currentPage++;
}

// Search through workflow artifacts until we find a workflow run w/ artifact name that we are looking for
for (const workflowRun of completedWorkflowRuns) {
core.info(`Checking artifacts for workflow run: ${workflowRun.html_url}`);

const {
data: { artifacts },
} = await octokit.rest.actions.listWorkflowRunArtifacts({
owner,
repo,
run_id: workflowRun.id,
});

if (!artifacts) {
core.warning(
`Unable to fetch artifacts for branch: ${branch}, workflow: ${workflow_id}, workflowRunId: ${workflowRun.id}`,
);
} else {
const foundArtifact = artifacts.find(({ name }) => name === artifactName);
if (foundArtifact) {
core.info(`Found suitable artifact: ${foundArtifact.url}`);
return {
artifact: foundArtifact,
workflowRun,
};
}
}
}

core.warning(`Artifact not found: ${artifactName}`);
core.endGroup();
return null;
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
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
182 changes: 97 additions & 85 deletions dev-packages/size-limit-gh-action/index.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,12 +31,27 @@ class SizeLimit {
return bytes.format(size, { unitSeparator: ' ' });
}

formatTime(seconds) {
if (seconds >= 1) {
return `${Math.ceil(seconds * 10) / 10} s`;
formatPercentageChange(base = 0, current = 0) {
if (base === 0) {
return 'added';
}

if (current === 0) {
return 'removed';
}

const value = ((current - base) / base) * 100;
const formatted = (Math.sign(value) * Math.ceil(Math.abs(value) * 100)) / 100;

if (value > 0) {
return `+${formatted}%`;
}

if (value === 0) {
return '-';
}

return `${Math.ceil(seconds * 1000)} ms`;
return `${formatted}%`;
}

formatChange(base = 0, current = 0) {
Expand All@@ -48,75 +63,50 @@ class SizeLimit {
return 'removed';
}

const value = ((current - base) / base) * 100;
const formatted = (Math.sign(value) * Math.ceil(Math.abs(value) * 100)) / 100;
const value = current - base;
const formatted = this.formatBytes(value);

if (value > 0) {
return `+${formatted}% 🔺`;
return `+${formatted} 🔺`;
}

if (value === 0) {
return `${formatted}%`;
return '-';
}

return `${formatted}% 🔽`;
return `${formatted} 🔽`;
}

formatLine(value, change) {
return `${value} (${change})`;
}

formatSizeResult(name, base, current) {
return [name, this.formatLine(this.formatBytes(current.size), this.formatChange(base.size, current.size))];
}

formatTimeResult(name, base, current) {
return [
name,
this.formatLine(this.formatBytes(current.size), this.formatChange(base.size, current.size)),
this.formatLine(this.formatTime(current.loading), this.formatChange(base.loading, current.loading)),
this.formatLine(this.formatTime(current.running), this.formatChange(base.running, current.running)),
this.formatTime(current.total),
this.formatBytes(current.size),
this.formatPercentageChange(base.size, current.size),
this.formatChange(base.size, current.size),
];
}

parseResults(output) {
const results = JSON.parse(output);

return results.reduce((current, result) => {
let time = {};

if (result.loading !== undefined && result.running !== undefined) {
const loading = +result.loading;
const running = +result.running;

time = {
running,
loading,
total: loading + running,
};
}

return {
// biome-ignore lint/performance/noAccumulatingSpread: <explanation>
...current,
[result.name]: {
name: result.name,
size: +result.size,
...time,
},
};
}, {});
}

hasSizeChanges(base, current, threshold = 0) {
const names = [...new Set([...(base ? Object.keys(base) : []), ...Object.keys(current)])];
const isSize = names.some(name => current[name] && current[name].total === undefined);

// Always return true if time results are present
if (!isSize) {
return true;
}

return !!names.find(name => {
const baseResult = base?.[name] || EmptyResult;
Expand All@@ -132,16 +122,12 @@ class SizeLimit {

formatResults(base, current) {
const names = [...new Set([...(base ? Object.keys(base) : []), ...Object.keys(current)])];
const isSize = names.some(name => current[name] && current[name].total === undefined);
const header = isSize ? SIZE_RESULTS_HEADER : TIME_RESULTS_HEADER;
const header = SIZE_RESULTS_HEADER;
const fields = names.map(name => {
const baseResult = base?.[name] || EmptyResult;
const currentResult = current[name] || EmptyResult;

if (isSize) {
return this.formatSizeResult(name, baseResult, currentResult);
}
return this.formatTimeResult(name, baseResult, currentResult);
return this.formatSizeResult(name, baseResult, currentResult);
});

return [header, ...fields];
Expand All@@ -165,15 +151,11 @@ async function execSizeLimit() {
return { status, output };
}

const SIZE_RESULTS_HEADER = ['Path', 'Size'];
const TIME_RESULTS_HEADER = ['Path', 'Size', 'Loading time (3g)', 'Running time (snapdragon)', 'Total time'];
const SIZE_RESULTS_HEADER = ['Path', 'Size', '% Change', 'Change'];

const EmptyResult = {
name: '-',
size: 0,
running: 0,
loading: 0,
total: 0,
};

async function run() {
Expand DownExpand Up@@ -227,6 +209,8 @@ async function run() {
// Else, we run size limit for the current branch, AND fetch it for the comparison branch
let base;
let current;
let baseIsNotLatest = false;
let baseWorkflowRun;

try {
const artifacts = await getArtifactsForBranchAndWorkflow(octokit, {
Expand All@@ -240,6 +224,8 @@ async function run() {
throw new Error('No artifacts found');
}

baseWorkflowRun = artifacts.workflowRun;

await downloadOtherWorkflowArtifact(octokit, {
...repo,
artifactName: ARTIFACT_NAME,
Expand All@@ -248,6 +234,11 @@ async function run() {
});

base = JSON.parse(await fs.readFile(resultsFilePath, { encoding: 'utf8' }));

if (!artifacts.isLatest) {
baseIsNotLatest = true;
core.info('Base artifact is not the latest one. This may lead to incorrect results.');
Comment thread
mydea marked this conversation as resolved.
}
} catch (error) {
core.startGroup('Warning, unable to find base results');
core.error(error);
Expand All@@ -271,7 +262,22 @@ async function run() {
isNaN(thresholdNumber) || limit.hasSizeChanges(base, current, thresholdNumber) || sizeLimitComment;

if (shouldComment) {
const body = [SIZE_LIMIT_HEADING, markdownTable(limit.formatResults(base, current))].join('\r\n');
const bodyParts = [SIZE_LIMIT_HEADING];

if (baseIsNotLatest) {
bodyParts.push(
'⚠️ **Warning:** Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.',
);
}

bodyParts.push(markdownTable(limit.formatResults(base, current)));

if (baseWorkflowRun) {
bodyParts.push('');
bodyParts.push(`[View base workflow run](${baseWorkflowRun.html_url})`);
}

const body = bodyParts.join('\r\n');

try {
if (!sizeLimitComment) {
Expand DownExpand Up@@ -320,7 +326,7 @@ const DEFAULT_PAGE_LIMIT = 10;
* This is a bit hacky since GitHub Actions currently does not directly
* support downloading artifacts from other workflows
*/
export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, workflowName, branch, artifactName }) {
async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, workflowName, branch, artifactName }) {
core.startGroup(`getArtifactsForBranchAndWorkflow - workflow:"${workflowName}", branch:"${branch}"`);

let repositoryWorkflow = null;
Expand DownExpand Up@@ -361,14 +367,13 @@ export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, w
const workflow_id = repositoryWorkflow.id;

let currentPage = 0;
const completedWorkflowRuns = [];
let latestWorkflowRun = null;

for await (const response of octokit.paginate.iterator(octokit.rest.actions.listWorkflowRuns, {
owner,
repo,
workflow_id,
branch,
status: 'completed',
per_page: DEFAULT_PAGE_LIMIT,
event: 'push',
})) {
Expand All@@ -379,12 +384,47 @@ export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, w
}

// Do not allow downloading artifacts from a fork.
completedWorkflowRuns.push(
...response.data.filter(workflowRun => workflowRun.head_repository.full_name === `${owner}/${repo}`),
);
const filtered = response.data.filter(workflowRun => workflowRun.head_repository.full_name === `${owner}/${repo}`);

// Sort to ensure the latest workflow run is the first
filtered.sort((a, b) => {
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
});

// Store the first workflow run, to determine if this is the latest one...
if (!latestWorkflowRun) {
latestWorkflowRun = filtered[0];
}

// Search through workflow artifacts until we find a workflow run w/ artifact name that we are looking for
for (const workflowRun of filtered) {
core.info(`Checking artifacts for workflow run: ${workflowRun.html_url}`);

if (completedWorkflowRuns.length) {
break;
const {
data: { artifacts },
} = await octokit.rest.actions.listWorkflowRunArtifacts({
owner,
repo,
run_id: workflowRun.id,
});

if (!artifacts) {
core.warning(
`Unable to fetch artifacts for branch: ${branch}, workflow: ${workflow_id}, workflowRunId: ${workflowRun.id}`,
);
} else {
const foundArtifact = artifacts.find(({ name }) => name === artifactName);
if (foundArtifact) {
core.info(`Found suitable artifact: ${foundArtifact.url}`);
return {
artifact: foundArtifact,
workflowRun,
isLatest: latestWorkflowRun.id === workflowRun.id,
};
} else {
core.info(`No artifact found for ${artifactName}, trying next workflow run...`);
}
}
}

if (currentPage > DEFAULT_MAX_PAGES) {
Expand All@@ -396,34 +436,6 @@ export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, w
currentPage++;
}

// Search through workflow artifacts until we find a workflow run w/ artifact name that we are looking for
for (const workflowRun of completedWorkflowRuns) {
core.info(`Checking artifacts for workflow run: ${workflowRun.html_url}`);

const {
data: { artifacts },
} = await octokit.rest.actions.listWorkflowRunArtifacts({
owner,
repo,
run_id: workflowRun.id,
});

if (!artifacts) {
core.warning(
`Unable to fetch artifacts for branch: ${branch}, workflow: ${workflow_id}, workflowRunId: ${workflowRun.id}`,
);
} else {
const foundArtifact = artifacts.find(({ name }) => name === artifactName);
if (foundArtifact) {
core.info(`Found suitable artifact: ${foundArtifact.url}`);
return {
artifact: foundArtifact,
workflowRun,
};
}
}
}

core.warning(`Artifact not found: ${artifactName}`);
core.endGroup();
return null;
Expand Down