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} 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} 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 }