Compare commits

...

1 commit

Author SHA1 Message Date
9210c9a29b
WIP: refactor: port to JS
Some checks failed
/ check (treefmt) (push) Failing after 19s
/ report-size (push) Has been skipped
/ report-download-check (push) Has been skipped
This should hopefully reduce the complexity of the action
2025-07-10 23:57:20 +02:00
20 changed files with 215057 additions and 108 deletions

View file

@ -2,7 +2,7 @@
end_of_line = lf end_of_line = lf
charset = utf-8 charset = utf-8
[*.{nix,json,sh}] [*.{nix,json,sh,js}]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
trim_trailing_whitespace = true trim_trailing_whitespace = true

View file

@ -8,7 +8,7 @@ jobs:
check: check:
- treefmt - treefmt
steps: steps:
- uses: "https://git.salame.cl/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683" # v4 - uses: 'https://git.salame.cl/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # v4
- name: Run checks - name: Run checks
run: | run: |
nix --version nix --version
@ -18,7 +18,7 @@ jobs:
runs-on: nixos runs-on: nixos
needs: check needs: check
steps: steps:
- uses: "https://git.salame.cl/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683" # v4 - uses: 'https://git.salame.cl/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # v4
- run: nix --version - run: nix --version
- name: Create Size Report - name: Create Size Report
uses: ./ uses: ./

View file

@ -7,7 +7,7 @@ jobs:
check-renovaterc: check-renovaterc:
runs-on: nixos runs-on: nixos
steps: steps:
- uses: "https://git.salame.cl/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683" # v4 - uses: 'https://git.salame.cl/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # v4
- name: Validate renovaterc.json - name: Validate renovaterc.json
run: | run: |
nix --version nix --version

3
.gitattributes vendored Normal file
View file

@ -0,0 +1,3 @@
* text=auto eol=lf
dist/** -diff linguist-generated=true

103
.gitignore vendored
View file

@ -1 +1,104 @@
result* result*
# Dependency directory
node_modules
# Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# OS metadata
.DS_Store
Thumbs.db
# Ignore built ts files
__tests__/runner/*
# IDE files
.idea
*.code-workspace

5
.prettierignore Normal file
View file

@ -0,0 +1,5 @@
.DS_Store
.licenses/
dist/
node_modules/
coverage/

16
.prettierrc.yml Normal file
View file

@ -0,0 +1,16 @@
# See: https://prettier.io/docs/en/configuration
printWidth: 80
tabWidth: 2
useTabs: false
semi: false
singleQuote: true
quoteProps: as-needed
jsxSingleQuote: false
trailingComma: none
bracketSpacing: true
bracketSameLine: true
arrowParens: always
proseWrap: always
htmlWhitespaceSensitivity: css
endOfLine: lf

View file

@ -2,9 +2,11 @@
Use `nix path-info` to query the size of flake outputs and produce a report. Use `nix path-info` to query the size of flake outputs and produce a report.
This repost can be posted to a PR (as formatted markdown) and/or uploaded as a workflow artifact. This repost can be posted to a PR (as formatted markdown) and/or uploaded as a
workflow artifact.
Requires `nix`, `jq`, `curl`, `sed`, `gunzip`, `tar` and `coreutils` to be in the runner's path. Requires `nix`, `jq`, `curl`, `sed`, `gunzip`, `tar` and `coreutils` to be in
the runner's path.
## Example ## Example
@ -39,26 +41,32 @@ For more details see the [action.yaml](./action.yml) file.
**Definitions:** **Definitions:**
- `Name`: the name of the package/configuration. - `Name`: the name of the package/configuration.
- `Size`: the closure size (size on disk/NAR size + all transitive dependencies). - `Size`: the closure size (size on disk/NAR size + all transitive
dependencies).
- `NAR Size`: the size of the build output (package without the dependencies). - `NAR Size`: the size of the build output (package without the dependencies).
- `[NAR] Size Change`: the amount changed compared to the main branch. - `[NAR] Size Change`: the amount changed compared to the main branch.
**Tips on reading this data:** **Tips on reading this data:**
- For NixOS configurations you generally care only about the `Size` (closure size/size on disk). - For NixOS configurations you generally care only about the `Size` (closure
size/size on disk).
- Reduce the `Size` by disabling unneeded services/default packages. - Reduce the `Size` by disabling unneeded services/default packages.
- For Packages you care about both the `Size` and the `NAR Size`. - For Packages you care about both the `Size` and the `NAR Size`.
- Reduce the `NAR Size` by reducing the size of the build outputs, e.g. don't copy unnecessary data to the $out dir, optimize binaries for size, etc. - Reduce the `NAR Size` by reducing the size of the build outputs, e.g. don't
copy unnecessary data to the $out dir, optimize binaries for size, etc.
- Reduce the `Size` by reducing the dependencies (e.g. `buildInputs`). - Reduce the `Size` by reducing the dependencies (e.g. `buildInputs`).
- Don't worry too much about size, some dependencies are deduplicated, e.g. `glibc` adds ~40MiB to the `Size`, but is generally shared by ~every binary on the system, so, chances are, you are already including it from somewhere else and statically linking with e.g. `musl` is not gonna improve things. - Don't worry too much about size, some dependencies are deduplicated, e.g.
`glibc` adds ~40MiB to the `Size`, but is generally shared by ~every binary
on the system, so, chances are, you are already including it from somewhere
else and statically linking with e.g. `musl` is not gonna improve things.
# NixOS Configurations # NixOS Configurations
| Name | Size | Size Change | NAR Size | NAR Size Change | | Name | Size | Size Change | NAR Size | NAR Size Change |
|------|-----:|------------:|---------:|----------------:| | -------- | ----: | ----------: | -------: | --------------: |
| `gemini` | 11Gi | -2.4Mi | 28Ki | 0 | | `gemini` | 11Gi | -2.4Mi | 28Ki | 0 |
| `leo` | 1.6Gi | 0 | 25Ki | 0 | | `leo` | 1.6Gi | 0 | 25Ki | 0 |
| `libra` | 9.4Gi | -2.4Mi | 28Ki | 0 | | `libra` | 9.4Gi | -2.4Mi | 28Ki | 0 |
| `taurus` | 7.6Gi | 0 | 34Ki | 0 | | `taurus` | 7.6Gi | 0 | 34Ki | 0 |
</details> </details>

View file

@ -32,6 +32,9 @@ inputs:
This is a no-op in case no PR is associated with the current branch. This is a no-op in case no PR is associated with the current branch.
default: 'true' default: 'true'
system:
description: |
The nix system name to query the packages of (e.g. x86_64-linux)
# Generate workflow artifact # Generate workflow artifact
generate-artifact: generate-artifact:
description: Export the generated markdown document as a workflow artifact. description: Export the generated markdown document as a workflow artifact.
@ -65,94 +68,5 @@ inputs:
default: ${{ github.base_ref }} default: ${{ github.base_ref }}
outputs: outputs:
runs: runs:
using: 'composite' using: 'nodejs20'
steps: main: dist/index.js
- name: Find PR (if it exists)
id: pr-number
if: inputs.comment-on-pr == 'true'
run: |
. "$GITHUB_ACTION_PATH/scripts/utils.sh"
log 'Determine head_ref'
# For push & tag events it'll bet GITHUB_REF_NAME, for pull_request events it'll be GITHUB_HEAD_REF
head_ref=${GITHUB_REF_NAME:-$GITHUB_HEAD_REF}
log "Get PR number for $head_ref"
prs=$(curl -X 'GET' \
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/pulls?state=open&sort=recentupdate" \
-H "Authorization: token $GITHUB_TOKEN" \
-H 'Accept: application/json')
pr_number=$(echo "$prs" |
jq --arg head_ref "$head_ref" '.[] | select(.head.ref == $head_ref) | .number')
# This seems to create the file???
log "GITHUB_OUTPUT=$GITHUB_OUTPUT"
log "$(ls -l "$GITHUB_OUTPUT")"
# Protect against running before a PR is made or if it is triggered on the main branch
if [ -z "$pr_number" ]; then
warn "No PR created for this commit"
echo "pr-number=" >> "$GIHUB_OUTPUT"
exit 0
fi
log "Retrieved index: $pr_number"
log "Expected PR URL: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/pulls/$pr_number"
echo "pr-number=$pr_number" >> "$GITHUB_OUTPUT"
- name: Find previous comment (if present)
# We want to generate a comment, and we we able to fin the PR number
if: inputs.comment-on-pr == 'true' && steps.pr-number.outputs.pr-number != ''
id: find-comment
uses: https://github.com/peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3
with:
issue-number: ${{ steps.pr-number.outputs.pr-number }}
direction: first
body-includes: "<!-- AUTOGENERATED by nix-flake-outputs-size action -->"
- name: Create report
if: inputs.comment-on-pr == 'true' || inputs.generate-artifact == 'true'
env:
PR_ID: ${{ steps.pr-number.outputs.pr-number }}
COMMENT: ${{ inputs.comment-on-pr }}
COMMENT_ID: ${{ steps.find-comment.outputs.comment-id }}
ARTIFACT_NAME: ${{ inputs.artifact-name }}
DO_COMPARISON: ${{ inputs.do-comparison }}
BASE_BRANCH: ${{ inputs.base-branch }}
JOB_NAME: ${{ inputs.job-name }}
run: |
. "$GITHUB_ACTION_PATH/scripts/utils.sh"
# Input validation
if [ "$DO_COMPARISON" = 'true' ] && [ -z "$JOB_NAME" ]; then
error 'job-name should be set if you want to generate a comparison report'
exit 1
fi
# Create Size Report
"$GITHUB_ACTION_PATH/scripts/create-report.sh" report.json
# Nothing else to do
if [ "$COMMENT" != 'true' ]; then exit 0; fi
# Try to do a comparison report
if [ "$DO_COMPARISON" = 'true' ]; then
if "$GITHUB_ACTION_PATH/scripts/retrieve-old-report.sh" && [ -f old-report.json ]; then
log "Reporting on sizes and comparing to sizes in $HEAD_BRANCH"
"$GITHUB_ACTION_PATH/scripts/comment_on_pr.sh" report.json old-report.json
exit 0
else
error 'Failed to do comparison, fallback to posting the report without them'
fi
fi
# Just report values
log 'Reporting on sizes'
"$GITHUB_ACTION_PATH/scripts/comment_on_pr.sh" report.json
- name: Upload Artifact
uses: https://git.salame.cl/actions/upload-artifact@v4
if: inputs.generate-artifact == 'true'
with:
path: report.json
name: ${{ inputs.artifact-name }}

202419
dist/index.js generated vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/index.js.map generated vendored Normal file

File diff suppressed because one or more lines are too long

61
eslint.config.mjs Normal file
View file

@ -0,0 +1,61 @@
// See: https://eslint.org/docs/latest/use/configure/configuration-files
import { fixupPluginRules } from '@eslint/compat'
import { FlatCompat } from '@eslint/eslintrc'
import js from '@eslint/js'
import _import from 'eslint-plugin-import'
import jest from 'eslint-plugin-jest'
import prettier from 'eslint-plugin-prettier'
import globals from 'globals'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
})
export default [
{
ignores: ['**/coverage', '**/dist', '**/linter', '**/node_modules']
},
...compat.extends(
'eslint:recommended',
'plugin:jest/recommended',
'plugin:prettier/recommended'
),
{
plugins: {
import: fixupPluginRules(_import),
jest,
prettier
},
languageOptions: {
globals: {
...globals.node,
...globals.jest,
Atomics: 'readonly',
SharedArrayBuffer: 'readonly'
},
ecmaVersion: 2023,
sourceType: 'module'
},
rules: {
camelcase: 'off',
'eslint-comments/no-use': 'off',
'eslint-comments/no-unused-disable': 'off',
'i18n-text/no-en': 'off',
'import/no-namespace': 'off',
'no-console': 'off',
'no-shadow': 'off',
'no-unused-vars': 'off',
'prettier/prettier': 'error'
}
}
]

View file

@ -26,7 +26,7 @@
# Setup formatters # Setup formatters
treefmt = { treefmt = {
# Ignore images # Ignore images
settings.global.excludes = [ "*.png" ]; settings.global.excludes = [ "*.png" "dist/"];
projectRootFile = "flake.nix"; projectRootFile = "flake.nix";
programs = { programs = {
mdformat.enable = true; mdformat.enable = true;

30
jest.config.js Normal file
View file

@ -0,0 +1,30 @@
// See: https://jestjs.io/docs/configuration
/** @type {import('jest').Config} */
const jestConfig = {
clearMocks: true,
collectCoverage: true,
collectCoverageFrom: ['./src/**'],
coverageDirectory: './coverage',
coveragePathIgnorePatterns: ['/node_modules/', '/dist/'],
coverageReporters: ['json-summary', 'text', 'lcov'],
// Uncomment the below lines if you would like to enforce a coverage threshold
// for your action. This will fail the build if the coverage is below the
// specified thresholds.
// coverageThreshold: {
// global: {
// branches: 100,
// functions: 100,
// lines: 100,
// statements: 100
// }
// },
moduleFileExtensions: ['js'],
reporters: ['default'],
testEnvironment: 'node',
testMatch: ['**/*.test.js'],
testPathIgnorePatterns: ['/dist/', '/node_modules/'],
verbose: true
}
export default jestConfig

12141
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

66
package.json Normal file
View file

@ -0,0 +1,66 @@
{
"name": "nix-flake-outputs-size-report",
"description": "Use 'nix-path-info' to query the size of outputs and produce a markdown report",
"version": "0.1.0",
"author": "Jalil David Salamé Messina",
"type": "module",
"private": true,
"homepage": "https://git.salame.cl/jalil/nix-flake-outputs-size#readme",
"repository": {
"type": "git",
"url": "git+https://git.salame.cl/jalil/nix-flake-outputs-size.git"
},
"bugs": {
"url": "https://git.salame.cl/jalil/nix-flake-outputs-size/issues"
},
"keywords": [
"actions"
],
"exports": {
".": "./dist/index.js"
},
"engines": {
"node": ">=20"
},
"scripts": {
"bundle": "npm run format:write && npm run package",
"ci-test": "NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 npx jest",
"coverage": "npx make-coverage-badge --output-path ./badges/coverage.svg",
"format:write": "npx prettier --write .",
"format:check": "npx prettier --check .",
"lint": "npx eslint .",
"local-action": "npx @github/local-action . src/main.js .env",
"package": "npx rollup --config rollup.config.js",
"package:watch": "npm run package -- --watch",
"test": "NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 npx jest",
"all": "npm run format:write && npm run lint && npm run test && npm run coverage && npm run package"
},
"license": "MIT",
"dependencies": {
"@actions/artifact": "^2.3.2",
"@actions/core": "^1.11.1",
"@actions/exec": "^1.1.1",
"@actions/github": "^6.0.1"
},
"devDependencies": {
"@eslint/compat": "^1.3.1",
"@github/local-action": "^3.2.1",
"@jest/globals": "^30.0.4",
"@rollup/plugin-commonjs": "^28.0.6",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.1",
"eslint": "^9.30.1",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jest": "^29.0.1",
"eslint-plugin-prettier": "^5.5.1",
"jest": "^30.0.4",
"make-coverage-badge": "^1.2.0",
"prettier": "^3.6.2",
"prettier-eslint": "^16.4.2",
"rollup": "^4.44.2"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "*"
}
}

11
report.json Normal file
View file

@ -0,0 +1,11 @@
{
"packages": [
{
"name": "hello",
"size": 33159640,
"narSize": 234680
}
],
"nixosConfigurations": [],
"homeConfigurations": []
}

18
rollup.config.js Normal file
View file

@ -0,0 +1,18 @@
// See: https://rollupjs.org/introduction/
import commonjs from '@rollup/plugin-commonjs'
import json from '@rollup/plugin-json'
import { nodeResolve } from '@rollup/plugin-node-resolve'
const config = {
input: 'src/index.js',
output: {
esModule: true,
file: 'dist/index.js',
format: 'es',
sourcemap: true
},
plugins: [commonjs(), json(), nodeResolve({ preferBuiltins: true })]
}
export default config

8
src/index.js Normal file
View file

@ -0,0 +1,8 @@
/**
* The entrypoint for the action. This file simply imports and runs the action's
* main logic.
*/
import { run } from './main.js'
/* istanbul ignore next */
run()

145
src/main.js Normal file
View file

@ -0,0 +1,145 @@
import * as core from '@actions/core'
import * as exec from '@actions/exec'
import * as github from '@actions/github'
import { DefaultArtifactClient } from '@actions/artifact'
import * as fs from 'node:fs/promises'
/**
* The main function for the action.
*
* @returns {Promise<void>} Resolves when the action is complete.
*/
export async function run() {
try {
const comment = core.getBooleanInput('comment-on-pr', { required: false })
const upload = core.getBooleanInput('generate-artifact', {
required: false
})
const artifactName = core.getInput('artifact-name', { required: false })
const compare = core.getBooleanInput('do-comparison', { required: false })
const jobName = core.getInput('job-name', { required: false })
const baseBranch = core.getInput('base-branch', { required: false })
const system = core.getInput('system', { required: true })
if (!comment && !upload) {
core.error(
'Both comment-on-pr and generate-artifact were set to false, nothing to do, consider disabling the action instead'
)
core.setFailed('Neither commenting nor uploading a report ... why?')
return
}
const flakeInfo = JSON.parse(
await collectOutput('nix', ['flake', 'show', '--json'])
)
core.debug(`nix flake show --json: ${flakeInfo}`)
const report = await core.group('Generating size report', () =>
generateReport(flakeInfo, system)
)
if (upload) {
const artifact = new DefaultArtifactClient()
await fs.writeFile(artifactName, JSON.stringify(report))
const { id, size } = artifact.uploadArtifact(artifactName, [artifactName])
core.info(`Uploaded report ${artifactName} (${size}B) with id ${id}`)
}
// Done
if (!comment) {
return
}
// TODO: compare reports and create comment
} catch (error) {
// Fail the workflow run if an error occurs
if (error instanceof Error) core.setFailed(error.message)
}
}
async function generateReport(flakeInfo, system) {
const packages = getPackages(flakeInfo, system)
const hmConfigs = getKeys(flakeInfo, 'homeConfigurations')
const nixosConfigs = getKeys(flakeInfo, 'nixosConfigurations')
core.info(`packages: ${packages}`)
core.info(`homeConfigurations: ${hmConfigs}`)
core.info(`nixosConfigurations: ${nixosConfigs}`)
const pkgSizes = await core.group('Calculating size of packages', () =>
calculateSizeOf(packages, (pkg) => `.#${pkg}`)
)
const hmConfigsSizes = await core.group(
'Calculating size of Home-Manager Configurations',
() =>
calculateSizeOf(
hmConfigs,
(config) => `.#homeConfigurations.${config}.activationPackages`
)
)
const nixosConfigsSizes = await core.group(
'Calculating size of NixOS Configurations',
() =>
calculateSizeOf(
nixosConfigs,
(config) =>
`.#nixosConfigurations.${config}.config.system.build.toplevel`
)
)
return {
packages: pkgSizes,
nixosConfigurations: nixosConfigsSizes,
homeConfigurations: hmConfigsSizes
}
}
async function calculateSizeOf(names, nameToInstallable) {
let sizes = []
for (const name of names) {
sizes.push(await calculateSize(nameToInstallable(name)))
}
return sizes
}
/**
* Get the packages from a `nix flake show --json` blob
*
* @returns {Array<String>} the packages in the current flake for the specific system
*/
function getPackages(flakeInfo, system) {
return 'packages' in flakeInfo ? getKeys(flakeInfo.packages, system) : []
}
function getKeys(flakeInfo, key) {
return key in flakeInfo ? Object.keys(flakeInfo[key]) : []
}
async function calculateSize(installable) {
const path = await collectOutput('nix', [
'build',
'--print-out-paths',
installable
])
const data = JSON.parse(
await collectOutput('nix', ['path-info', '--closure-size', '--json', path])
)
data.path = path
return data
}
async function collectOutput(cmd, args) {
let output = ''
await exec.exec(cmd, args, {
listeners: {
stdout: (data) => {
output += data.toString()
}
}
})
return output
}