Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

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

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

Already on GitHub? Sign in to your account

Add project hash information to nx print-affected output #3292

Closed
zakhenry opened this issue Jul 9, 2020 · 33 comments
Closed

Add project hash information to nx print-affected output #3292

zakhenry opened this issue Jul 9, 2020 · 33 comments
Labels
scope: core core nx functionality type: feature

Comments

@zakhenry
Copy link
Contributor

zakhenry commented Jul 9, 2020

Description

I'd like to see a set of fields added to the print affected json output, something like

{
  "projects": [...],
  "projectGraph": {...},
  "hashes": {
    "my-app": {
       "current": "4b669d95627bf6724502d7b4ff6f41d3654c50ba9e3b1cf31e72c138370423f8",
       "previous": "0bdb20fbd1788957302f4cf356ff0449ee3c2d10960814f6c34fd399fa64f5d3"
    }
  }
}

This hash key should include all projects, not just the affected ones.

Motivation

I use nx print-affected in order to dynamically generate a CI pipeline which works amazingly well, however I have a need to determine the current hash of an unchanged project so that I can fetch the docker container

Suggested Implementaion

I assume that the affected projects is determined by some hashing already, so it must be feasible to add this into the affected output

Alternate Implementations

This could be a completely different command line tool if needed.

@vsavkin vsavkin added the scope: core core nx functionality label Aug 7, 2020
@github-actions
Copy link

This issue has been automatically marked as stale because it hasn't had any recent activity. It will be closed in 14 days if no further activity occurs.
If we missed this issue please reply to keep it active.
Thanks for being a part of the Nx community! 🙏

@github-actions github-actions bot added the stale label Mar 29, 2021
@maxime1992
Copy link

up

@github-actions github-actions bot removed the stale label Apr 2, 2021
@douglasward
Copy link

I would love to have this to improve our CI pipeline as well - would save a lot of homegrown hacks

@zakhenry
Copy link
Contributor Author

zakhenry commented Apr 9, 2021

There is a bit of a workaround for this which is to run yarn --silent nx print-affected --all --target=build, and in the output there is the follow object which contains the hash

{
  "tasks": [
    {
      "id": "lib-name:build",
      "overrides": {},
      "target": {
        "project": "lib-name",
        "target": "build"
      },
      "command": "yarn nx build lib-name",
      "outputs": [
        "dist/libs/lib-name"
      ],
      "hash": "3128ba521cbe508342017bff3a2429d3bfc00cb6d17269b2078209bd17992108"
    },
...etc

I use this in combination with other nx print-affected commands to work out what has changed, so at least I'm using the same internal calculation (and will hopefully benefit from the new smart hashing logic NX 12 brings, but haven't tried yet)

@douglasward
Copy link

Ahh awesome, thank you so much for this - this will certainly do for now!

@zakhenry
Copy link
Contributor Author

zakhenry commented Jun 21, 2021

Unfortunately this workaround was removed in 67e78dc#diff-715d57e669c659f3ed1b596f901efab8bb79d61e16149eb088768110af4eed68. @vsavkin is there a different workaround that we might be able to use instead?

@douglasward
Copy link

This is sad news indeed, I put a lot of work into rewriting our CI to use this workaround. @vsavkin are you able to shed some light on this? Is there another workaround or an official implementation being worked on?

@maxime1992
Copy link

I spent some time during a hackday to see if I could get something in our case to get back the hash on a per app/per lib case.

I did dug into the diff mentioned by @zakhenry, I also looked into "NX plugins and devkit", "NX plugins", "Creating Custom Executors", the affected function and the runAllTasks function (which uses the Hasher). Unfortunately, I don't think there's any way to export those directly without updating the code on NX side.

Plugins and custom executors won't be able to add something like/modify the affected command.

I also tried a dummy approach of using NX through a node script like this:

const nrwlWorkspace = require('@nrwl/workspace/src/command-line/affected');

const main = async () => {
  const affected = await nrwlWorkspace.affected('print-affected', {
    _: ['print-affected'],
    target: 'build',
    uncommitted: undefined,
    untracked: undefined,
    all: undefined,
    exclude: [],
    skipNxCache: false,
    onlyFailed: false,
    $0: '/home/maxime/Documents/cloudnc/code/frontend/node_modules/.bin/nx',
  });

  console.log(affected.tasks.map((task) => `${task.target.project} - ${task.hash}`));
};

main();

but unless I copy the whole affected file to modify it, I don't think there's any argument I can pass to tweak the output 🤔. And for obvious reason I don't want to do that (that'd be like maintaining a small fork of NX...).

I'm also confused by the commit that @zakhenry mentioned. This commit is called "feat(core): add support for custom hashers" so I'd expect the ability to customize the hashing strategy from a consumer point of view but maybe that's not it.

Anyway from here I've got 2 things to highlight:

  • I think it'd be nice to avoid a breaking change in the affected command. I remember @vsavkin saying he was trying real hard to avoid breaking changes in all the commands in the nrwl stream last week so this may just be something that they did't realize was used. This is also not documented anywhere as far as I'm aware so maybe calling this a breaking change is unfair but on the other hand it's in the output of a public command
  • Maybe it'd be interesting to have a system plugin at the highest possible level (like create custom commands?). So that someone could implement a command to give exactly what they need for example (nx my-custom-command). But this may go against nx philosophy, unsure

Thanks for any help on this 🙏 We're now stuck to nx 12.2.0 till we solve it as our CI pipeline relies on the hashes 🙏

@maxime1992
Copy link

Any idea on this? Any guidance on this? I get 1 day per month to do what I want (hackday).
I've spent the last one trying to dig into this issue (CF comment above) and I'm happy to spend the next one doing the same thing (tomorrow!). But I can't do it without a little guidance here, any word from someone in the NX team would be very welcome. We're hard stuck on nx 12.2.0 because of this so it starts to be really blocking to get new features (like config files being split per project instead of a massive one, etc).

As I said, I'm motivated to spend an entire day on this tomorrow if needed. If someone from NX could just help me out a little and give me a direction that'd be much appreciated 🙏

@douglasward
Copy link

If there isn't any plans to support exposing the hash again, which would be super sad, then maybe it is enough to use the Hasher in a custom script by importing it from import { Hasher } from '@nrwl/workspace/src/core/hasher/hasher';.

ng print-affected --target=build --all could provide the ProjectGraph and the tasks needed to get the hashes from the Hasher using the hashTaskWithDepsAndContext function.

e.g. something like

import { Hasher } from '@nrwl/workspace/src/core/hasher/hasher';

const hasher = new Hasher(projectGraph, nxConfig, {});
const hash = await hasher.hashTaskWithDepsAndContext(task);

@github-actions
Copy link

This issue has been automatically marked as stale because it hasn't had any recent activity. It will be closed in 14 days if no further activity occurs.
If we missed this issue please reply to keep it active.
Thanks for being a part of the Nx community! 🙏

@github-actions github-actions bot added the stale label Oct 11, 2022
@maxime1992
Copy link

Would still be nice to have. So bump for the stale label :)

@github-actions github-actions bot removed the stale label Oct 12, 2022
@Rijak0
Copy link

Rijak0 commented Oct 27, 2022

Thanks for the infos provided by everybody.
I'd also need the functionality for CI/CD.
I've played around a little, and to me it seems like this might do it:

> nx print-affected \
      --base=<first-commit-id-ever> \
      --head=HEAD \
      --target=build \

If I'm not mistaken, this should show the hash for all projects, because compared to the first commit ever, they're all affected.
Then simply use the result like this:

const resultPlain = await execute(command);
const result = JSON.parse(resultPlain);
// below doesn't consider e.g. :production for angular apps
const id = `${project}:${target}`;
return result?.tasks?.find((task) => task.id.startsWith(id))?.hash;

@maxime1992
Copy link

maxime1992 commented Oct 27, 2022

@Rijak0 which version of nx are you using? I think this issue slightly diverted from "It would be nice nice to have the hash for all projects" to "It'd be nice to have any hash at all" as they got removed completely from the output.

This issue has been going on for a while so I may have lost track a bit and have my thoughts confused but at least I think that's the case. So I don't understand how you'd get a result with a hash in your first command? I tried it in our project and here's one object from the returned array:

    {
      "id": "core-browser:build",
      "overrides": {
        "__overrides_unparsed__": []
      },
      "target": {
        "project": "core-browser",
        "target": "build"
      },
      "command": "yarn nx run core-browser:build",
      "outputs": [
        "dist/libs/core/browser",
        "libs/core/browser/dist",
        "libs/core/browser/build",
        "libs/core/browser/public"
      ]
    },

and there's no hash in here

@Rijak0
Copy link

Rijak0 commented Oct 27, 2022

Howdy @maxime1992!

> nx --version
14.8.3

"Works on my machine" :D

> nx print-affected --base=<my-first-commit-id> --head=HEAD --target=build

prints out

fatal: Path 'package.json' exists on disk, but not in '<my-first-commit-id>'.
// This error repeats several times
{
  "tasks": [
    {
      "id": "api-test:build",
      "overrides": {
        "__overrides_unparsed__": []
      },
      "target": {
        "project": "api-test",
        "target": "build"
      },
      "hash": "c6a5f8ec35b68a272f0e8060e068d46c0a8a8aa605562351149e76600ed41477",
      "command": "npx nx run api-test:build",
      "outputs": [
        "dist/apps/api-test"
      ]
    },
    ...

Interesting that it doesn't work for you.
I've changed one line in the project source code, the hash changed.
Changed it back, the hash then was the old one again.
Also updated the runtime config, and the hash also changed.
Looks promising 💯
What nx version are you using?

@Rijak0
Copy link

Rijak0 commented Oct 27, 2022

@maxime1992
Just double-checked, looks like I've found the perfect timing:
https://github.com/nrwl/nx/pull/11763/files#diff-08499af12ca814c9328ed32c05086090b6bc8b64681bb9e7d67600990eb64209
Guess an nx upgrade on your side will fix it :)

@maxime1992
Copy link

Wuuuuuuuuuuut that's fantastic 🤩! It doesn't fix the original issue here but it sure helps a bit at least! And with your workaround it might be something to get started with for sure. Thanks a lot!

@Rijak0
Copy link

Rijak0 commented Oct 27, 2022

Glad to give something back, perfect! 🎉
And if I'm not mistaken, it should create a hash for all projects (when compared to the first commit).
At least I hope so, because that's what we need 😆

@RobbinHabermehl
Copy link

Howdy @maxime1992!

> nx --version
14.8.3

"Works on my machine" :D

> nx print-affected --base=<my-first-commit-id> --head=HEAD --target=build

prints out

fatal: Path 'package.json' exists on disk, but not in '<my-first-commit-id>'.
// This error repeats several times
{
  "tasks": [
    {
      "id": "api-test:build",
      "overrides": {
        "__overrides_unparsed__": []
      },
      "target": {
        "project": "api-test",
        "target": "build"
      },
      "hash": "c6a5f8ec35b68a272f0e8060e068d46c0a8a8aa605562351149e76600ed41477",
      "command": "npx nx run api-test:build",
      "outputs": [
        "dist/apps/api-test"
      ]
    },
    ...

Interesting that it doesn't work for you. I've changed one line in the project source code, the hash changed. Changed it back, the hash then was the old one again. Also updated the runtime config, and the hash also changed. Looks promising 💯 What nx version are you using?

Unfortunately in v15 the hash is no longer present in this output.

@Rijak0
Copy link

Rijak0 commented Nov 24, 2022

Still working for me with v 15.2.1:

> nx print-affected --base=<first-commit-id> --head=HEAD --target=build

{
  "tasks": [
    {
      "id": "test:build",
      "overrides": {
        "__overrides_unparsed__": []
      },
      "target": {
        "project": "test",
        "target": "build"
      },
      "hash": "c0a1ad86001f38e371f65431729070bdacb6c402a9b2eed3becd81a7cc170f77",
      "command": "npx nx run test:build",
      "outputs": [
        "dist/packages/test"
      ]
    }
  ],
  ...
}

> nx --version
15.2.1

@iSuslov
Copy link

iSuslov commented May 20, 2023

Bumping this up. I need to know each hash to remove old caches and keep caches folder as small as possible.

PS: Although the solution above works, I've found a slightly better way of getting hashes. All such solutions are based on the same trick: we need nx to think that it needs to avoid cache and re-run everything for a particular target. Instead of checking out a particular commit, we can explicitly mark some file as changed by using --files flag.

Say you have defined your named inputs like this:

"namedInputs": {
    "sharedGlobals": [
      "{workspaceRoot}/package-lock.json"
    ],
    "default": ["{projectRoot}/src/**/*", "{projectRoot}/package.json", "sharedGlobals"]
  }

Now if you say your global file changed, it will think that everything is affected and provide you with all the current hashes:

nx print-affected -t build --select=tasks.hash --files=package-lock.json

Output:

c20ea272f7d6cbb776c7b3ccfbd5c80077c923fa9349d1f59206db6d9482b3c2, 678da0f7680d096aa820a55c7c066f1bd5710c44e81d5039ac53bd0013c40def, b37ee653589ee8078c477ecc0ee5c159c4ac0c8db5a4d8bb15abee2adc1bd5a8, 27288b76e312b4505a9cab699a8e79c857d01eefa824235df836cd88fe994e0e, 1d66809aa7f265908df28180a6be2f533de9569b6582d375771162b15dcb547b, 06ed2c0370ae0ca613289d1ec9898b1a1a681853e295f28d1b15ab178ac80231, 7f388226e67ce32c30d2e4e619265f4645851b4522d4937850711cf9cce1785e, bb5245c946350e20b25c434e9be619129b90cdddda54c81f46f17c014cc8c80f

@limeship-chd
Copy link

Seems like with nx v16 some logic to the hashing changed. The hash now looks quite different, e.g. 452563029102072530.

Also looks like print-affected is deprecated and being removed in v18. Since introducing the different hash structure, it also doesn't seem like the hash in print-affected works anymore. We did changes to the input files and the hash didn't change.

This is kind of a bummer, the hash really enabled some good automations and cicd options. I'll do some more digging and I'll try to find out what changed in between v15/16.

@maxime1992
Copy link

This should really be part of a public and stable API IMO. It's been working on and off too often.
If any maintainer read this, could you please consider having the hash somehow part of a stable API for the CLI please?

@Rijak0
Copy link

Rijak0 commented Sep 1, 2023 via email

@maxime1992
Copy link

that is a core functionality, our whole CI/CD is also depending on it

Exactly.

We‘re calculating affected differences with external systems like Docker,
Nuget, npm, for the build and deployment

We do the same. We take advantage of the fact that Gitlab is capable of having a first stage to generate a gitlab ci yaml file that is then run as the pipeline (aka dynamic pipelines on top of my head). So we have our first pipeline running as a deno script and checking the result of print-affected to analyse what's changed, and from there we generate all the pipeline (lint, test, e2e tests, docker builds, deployment etc) dynamically.

While the hashes are not necessary to do that, they are for further optimisations. For example, it'd be nice to save the docker build of projects by their hash. This way we could query in our first pipeline the registry to see if a tag already exist for a given hash, if so, completely skip a build because it's been done and whatever changed since, didn't affect the files for that project.

@limeship-chd
Copy link

@vsavkin @AgentEnder do you have any opinions on this? Some kind of stable api or cli to consistently get the hashes would be extremely helpful. Even adding it to the graph commands would be great (when outputting to json or similar) would work. Would gladly help with implementing this, if you have a vision for what the implementation should approx. look like 🙏

@Rijak0
Copy link

Rijak0 commented Sep 4, 2023

A little hacky, but seems to be working:

import {buildProjectGraphWithoutDaemon, createProjectGraphAsync} from "nx/src/project-graph/project-graph";
import {readNxJson} from "nx/src/config/nx-json";
import {createTaskGraph, mapTargetDefaultsToDependencies} from "nx/src/tasks-runner/create-task-graph";
import {InProcessTaskHasher} from "nx/src/hasher/task-hasher";
import {fileHasher} from "nx/src/hasher/file-hasher";
import {hashTask} from "nx/src/hasher/hash-task";
import {getProjectFileMap} from "nx/src/project-graph/build-project-graph";
import * as fs from "fs";

async function getHashes(target: string) {
    const projectGraph = await createProjectGraphAsync({exitOnError: true});
    const nxJson = readNxJson();

    // Necessary, so the other function has the projectFileMap set (nx internally)
    await buildProjectGraphWithoutDaemon();
    const projectFileMap = getProjectFileMap();

    const defaultDependencyConfigs = mapTargetDefaultsToDependencies(
        nxJson.targetDefaults
    );

    const projectNames = Object.values(projectGraph.nodes)
        .filter(p => !!p.data.targets[target])
        .map((p) => p.name);

    const taskGraph = createTaskGraph(projectGraph, defaultDependencyConfigs, projectNames, [target], undefined, {}, false);

    const hasher = new InProcessTaskHasher(
        projectFileMap.projectFileMap,
        projectFileMap.allWorkspaceFiles,
        projectGraph,
        nxJson,
        {__overrides_unparsed__: []},
        fileHasher
    );

    const tasks = Object.values(taskGraph.tasks);

    await Promise.all(
        tasks.map((t) => hashTask(hasher, projectGraph, taskGraph, t))
    );

    const result = tasks.map((task) => ({
        id: task.id,
        target: task.target,
        // hashDetails: task.hashDetails,
        hash: task.hash,
    }));
    
    return result;
}

async function run() {
    const hashes = await getHashes('build');

    console.log(JSON.stringify(hashes));

    await fs.promises.writeFile('logs/' + Date.now() + '.json', JSON.stringify(hashes, null, 2), 'utf-8');
}

run();

returns for a new playground nx workspace:

[
  {
    "id": "org-e2e:build:production",
    "target": {
      "project": "org-e2e",
      "target": "build",
      "configuration": "production"
    },
    "hash": "15189161131202036537"
  },
  {
    "id": "org:build:production",
    "target": {
      "project": "org",
      "target": "build",
      "configuration": "production"
    },
    "hash": "8069798034488914960"
  }
]
> nx --version
Nx Version:
- Local: v16.8.0-beta.7
- Global: Not found

@dsschneidermann
Copy link

import { readNxJson } from "nx/src/config/nx-json.js"
import { hashTask } from "nx/src/hasher/hash-task.js"
import { InProcessTaskHasher } from "nx/src/hasher/task-hasher.js"
import { getFileMap } from "nx/src/project-graph/build-project-graph.js"
import {
    buildProjectGraphWithoutDaemon,
    createProjectGraphAsync,
} from "nx/src/project-graph/project-graph.js"
import {
    createTaskGraph,
    mapTargetDefaultsToDependencies,
} from "nx/src/tasks-runner/create-task-graph.js"

// Code sourced from issue: https://github.com/nrwl/nx/issues/3292#issuecomment-1705419999
async function getHashes(target) {
    const projectGraph = await createProjectGraphAsync({ exitOnError: true })
    const nxJson = readNxJson()

    // Necessary, so the other function has the fileMap set (nx internally).
    await buildProjectGraphWithoutDaemon()
    const fileMap = getFileMap()

    const defaultDependencyConfigs = mapTargetDefaultsToDependencies(
        nxJson.targetDefaults
    )

    const projectNames = Object.values(projectGraph.nodes)
        .filter((p) => !!p.data.targets[target])
        .map((p) => p.name)

    const taskGraph = createTaskGraph(
        projectGraph,
        defaultDependencyConfigs,
        projectNames,
        [target],
        undefined,
        {},
        false
    )

    const hasher = new InProcessTaskHasher(
        fileMap.fileMap.projectFileMap,
        fileMap.allWorkspaceFiles,
        projectGraph,
        nxJson,
        { __overrides_unparsed__: [] }
    )

    const tasks = Object.values(taskGraph.tasks)

    await Promise.all(
        tasks.map((t) => hashTask(hasher, projectGraph, taskGraph, t))
    )

    const result = tasks.map((task) => ({
        id: task.id,
        target: task.target,
        // hashDetails: task.hashDetails,
        hash: task.hash,
    }))

    return result
}

async function run() {
    const hashes = await getHashes("build")

    process.stdout.write(`${JSON.stringify(hashes, null, 2)}\n`)
}

run()

For:

> npx nx --version
Nx Version:
- Local: v17.1.3
- Global: Not found

+1 to this being provided as standard output from one of the affected/graph commands.

@dsschneidermann
Copy link

v17.2.0 broke the script - fails on "getting externals", which may be due to rustReferences being new? I gave it a short attempt at fixing it but failed completely, and the error comes from native code so it's not at all clear to me what the problem is.

Stuck on v17.1 for now. @vsavkin Hopefully this can get some attention soon to unblock us - and I suspect many others.

Error: Failed to get external value
    at new NativeTaskHasherImpl (.../node_modules/nx/src/hasher/native-task-hasher-impl.js:26:23)
    at new InProcessTaskHasher (.../node_modules/nx/src/hasher/task-hasher.js:47:15)
    at getHashes (file:///.../scripts/nx_extract_hashes.mjs:41:20)
    at async run (file:///.../scripts/nx_extract_hashes.mjs:71:20) {
  code: 'InvalidArg'
}

@Inkdpixels
Copy link

Inkdpixels commented Dec 13, 2023

@dsschneidermann I am still on v17.1 due to other issues preventing me from an upgrade, but I adapted your example today to using the NX Deamon APIs, maybe this might work more stable with v17.2 and solves the issues you are seeing?

import { createProjectGraphAsync, ProjectGraph } from '@nx/devkit';
import { DaemonClient } from 'nx/src/daemon/client/client';
import { DaemonBasedTaskHasher } from 'nx/src/hasher/task-hasher';
import { createTaskGraph, mapTargetDefaultsToDependencies } from 'nx/src/tasks-runner/create-task-graph';
import { TargetDependencies, readNxJson } from 'nx/src/config/nx-json';

const uniq = <I,>(arr: I[]): I[] => [...new Set(arr))] as I[];
const resolveTargetHashes = async ({
  target,
  graph,
  dependencies,
  hasher,
}: {
  target: string;
  graph: ProjectGraph;
  dependencies: TargetDependencies;
  hasher: DaemonBasedTaskHasher;
}) => {
  const projectNames = Object.values(graph.nodes)
    .filter(({ data }) => !!data.targets?.[target])
    .map((p) => p.name);
  const taskGraph = createTaskGraph(graph, dependencies, projectNames, [target], undefined, {}, false);
  const tasks = Object.values(taskGraph.tasks);
  const hashes = await hasher.hashTasks(tasks, taskGraph, process.env);

  return hashes.map((res) => res.value);
};

async function exec() {
  const cwd = process.cwd();
  const nx = readNxJson(cwd);
  const graph = await createProjectGraphAsync({ exitOnError: true, resetDaemonClient: true });
  const daemon = new DaemonClient(nx);
  const hasher = new DaemonBasedTaskHasher(daemon, {});
  const dependencies = mapTargetDefaultsToDependencies(nx.targetDefaults);
  // Adapt the following line to your own likings, I automatically resolve all targets with caching enabled based on the targetDefaults.
  const targets = Object.entries(nx.targetDefaults ?? {})
    .filter(([target, config]) => config.cache === true)
    .map(([target]) => target);

  const hashes = await Promise.all(
    targets.map((target) => resolveTargetHashes({ target, graph, dependencies, hasher })),
  ).then((results) => uniq(results.flat()));

  console.log(hashes);
}

exec()
  .then(() => {
    process.exit(0);
  })
  .catch((err) => {
    console.error(err);
    process.exit(1);
  });

@aramfe
Copy link

aramfe commented Jan 24, 2024

Yes, functionality like this is truly needed. I hope someone on the team becomes aware of that. Ideally, we should be able to obtain the hash via a simple CLI command like:

nx get-hash myProject:myTarget

This way, nx would be much more powerful. Especially in the context of AWS, using hashes is crucial, as there isn't any other good indicator for checking whether or not a resource needs to be updated AND having some kind of indiciator about the deployments current build. Potential time savings could be enormous. For now, we have to use simple commit hashes, sadly.

From an infrastructure/deployment standpoint, this is more or less THE potential killer feature of why people would choose NX instead of Bazel or other solutions. However, as of now, NX simply doesn't provide this core functionality to fully utilize its incremental build system.

I would greatly appreciate it if the team could address this issue.

@aramfe
Copy link

aramfe commented Jan 24, 2024

I've updated the code of @Inkdpixels and made a little CLI tool out of it (also works with 17.2.8), which allows you to specifically filter by project and target. Output is the requested hash value.

import { createProjectGraphAsync, ProjectGraph } from "@nx/devkit"
import { DaemonClient } from "nx/src/daemon/client/client"
import { DaemonBasedTaskHasher } from "nx/src/hasher/task-hasher"
import { createTaskGraph, mapTargetDefaultsToDependencies } from "nx/src/tasks-runner/create-task-graph"
import { TargetDependencies, readNxJson } from "nx/src/config/nx-json"
import * as yargs from "yargs"

const argv = yargs
  .option("project", {
    alias: "p",
    description: "Filter result by project.",
    type: "string",
  })
  .option("target", {
    alias: "t",
    description: "Filter result by target.",
    type: "string",
  })
  .help()
  .alias("help", "h")
  .parseSync()

const resolveTargetHashes = async ({
  target,
  graph,
  dependencies,
  hasher,
}: {
  target: string
  graph: ProjectGraph
  dependencies: TargetDependencies
  hasher: DaemonBasedTaskHasher
}) => {
  const projectNames = Object.values(graph.nodes)
    .filter(({ data }) => !!data.targets?.[target])
    .map((p) => p.name)
  const taskGraph = createTaskGraph(graph, dependencies, projectNames, [target], undefined, {}, false)
  const tasks = Object.values(taskGraph.tasks)

  const result = await Promise.all(
    tasks.map((task) =>
      hasher.hashTask(task, taskGraph, process.env).then((hash) => ({
        id: task.id,
        target: task.target,
        hash: hash.value,
      })),
    ),
  )
  return result
}

async function exec() {
  const cwd = process.cwd()
  const nx = readNxJson(cwd)
  const graph = await createProjectGraphAsync({ exitOnError: true, resetDaemonClient: true })
  const daemon = new DaemonClient(nx)
  const hasher = new DaemonBasedTaskHasher(daemon, {})
  const dependencies = mapTargetDefaultsToDependencies(nx.targetDefaults)

  const targets = Object.entries(nx.targetDefaults ?? {})
    .filter(([target]) => target === argv.target)
    .map(([target]) => target)

  if (!targets.length) {
    console.error(`Project '${argv.project}' with target '${argv.target}' not found!`)
    process.exit(1)
  }

  const targetsWithHashes = await Promise.all(targets.map((target) => resolveTargetHashes({ target, graph, dependencies, hasher }))).then((results) => results.flat())

  const filteredTarget = targetsWithHashes.filter((target) => target.target.project === argv.project)

  if (!filteredTarget.length) {
    console.error(`Project '${argv.project}' with target '${argv.target}' not found!`)
    process.exit(1)
  }

  if (!filteredTarget[0].hash) {
    console.error(`No hash for target ${argv.target} of project ${argv.project} found`)
    process.exit(1)
  }

  console.log(filteredTarget[0].hash)
}

exec()
  .then(() => {
    process.exit(0)
  })
  .catch((err) => {
    console.error(err)
    process.exit(1)
  })

Usage:

$ ts-node getHash.ts -p myProject -t build

will result in:

3930834753735838015

@FrozenPandaz
Copy link
Collaborator

Hey all,

Nx cannot generate the correct hash for every task without actually running the command.

The tasks are hashed right before they executed in the middle of overall task execution. This is because tasks can depend on the outputs of tasks they depend on via https://nx.dev/reference/inputs#outputs-of-dependent-tasks. The same is true for custom hashers because Nx does not know what information custom hashers are hashing.. so it could be information which changes while other tasks execute.

We'll need to discuss more if this limitation is a deal breaker for this.

Nx could provide the hashes of only tasks which it knows for sure should be accurate. But it wouldn't be a definitive list of hashes which Nx will run. There are cases where none of the tasks in the command can actually be hashed upfront. So I wouldn't build anything on top of it such as spawning CI agents.

I think this discussion is better held at a higher level. What is the desired outcome of having the hashes? If we could discuss that, we could see if this information would suffice or if there is a better way to go about it.

@nrwl nrwl locked and limited conversation to collaborators Apr 2, 2024
@FrozenPandaz FrozenPandaz converted this issue into discussion #22623 Apr 2, 2024

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
scope: core core nx functionality type: feature
Projects
None yet
Development

No branches or pull requests