<commit_before>import { slugid } from 'taskcluster-client-web';
import groupBy from 'lodash/groupBy';

import { thPlatformMap } from '../helpers/constants';
import { createQueryParams } from '../helpers/url';
import { formatTaskclusterError } from '../helpers/errorMessage';
import { getProjectUrl } from '../helpers/location';

import PushModel from './push';
import TaskclusterModel from './taskcluster';

const uri = getProjectUrl('/jobs/');

// JobModel is the js counterpart of job
export default class JobModel {
  constructor(props) {
    Object.assign(this, props);
  }

  getTitle() {
    // we want to join the group and type information together
    // so we can search for it as one token (useful when
    // we want to do a search on something like `fxup-esr(`)
    let symbolInfo = this.job_group_symbol === '?' ? '' : this.job_group_symbol;
    symbolInfo += `(${this.job_type_symbol})`;

    return [
      thPlatformMap[this.platform] || this.platform,
      this.platform_option,
      this.job_group_name === 'unknown' ? undefined : this.job_group_name,
      this.job_type_name,
      symbolInfo,
    ]
      .filter(item => typeof item !== 'undefined')
      .join(' ');
  }

  getSearchStr() {
    return [
      this.getTitle(),
      this.ref_data_name,
      this.signature !== this.ref_data_name ? this.signature : undefined,
    ]
      .filter(item => typeof item !== 'undefined')
      .join(' ');
  }

  static getList(repoName, options, config) {
    // a static method to retrieve a list of JobModel
    config = config || {};
    const fetch_all = config.fetch_all || false;
    // The `uri` config allows to fetch a list of jobs from an arbitrary
    // endpoint e.g. the similar jobs endpoint. It defaults to the job
    // list endpoint.
    const jobUri = config.uri || uri;

    return fetch(`${jobUri}${options ? createQueryParams(options) : ''}`).then(
      async resp => {
        if (resp.ok) {
          const data = await resp.json();
          let itemList;
          let nextPagesJobs = [];

          // if the number of elements returned equals the page size, fetch the next pages
          if (fetch_all && data.results.length === data.meta.count) {
            const count = parseInt(data.meta.count, 10);
            const offset = parseInt(data.meta.offset, 10) + count;
            const newOptions = { ...options, offset, count };

            nextPagesJobs = await JobModel.getList(
              repoName,
              newOptions,
              config,
            );
          }
          if ('job_property_names' in data) {
            // the results came as list of fields
            // we need to convert them to objects
            itemList = data.results.map(
              elem =>
                new JobModel(
                  data.job_property_names.reduce(
                    (prev, prop, i) => ({ ...prev, [prop]: elem[i] }),
                    {},
                  ),
                ),
            );
          } else {
            itemList = data.results.map(job_obj => new JobModel(job_obj));
          }
          return [...itemList, ...nextPagesJobs];
        }
        const text = await resp.text();
        throw Error(text);
      },
    );
  }

  static get(repoName, pk, signal) {
    // a static method to retrieve a single instance of JobModel
    return fetch(`${uri}${pk}/`, { signal }).then(async response => {
      if (response.ok) {
        const job = await response.json();
        return new JobModel(job);
      }
      const text = await response.text();
      throw Error(`Loading job with id ${pk} : ${text}`);
    });
  }

  static getSimilarJobs(repoName, pk, options, config) {
    config = config || {};
    // The similar jobs endpoints returns the same type of objects as
    // the job list endpoint, so let's reuse the getList method logic.
    config.uri = `${uri}${pk}/similar_jobs/`;
    return JobModel.getList(repoName, options, config);
  }

  static async retrigger(jobs, repoName, notify) {
    const jobTerm = jobs.length > 1 ? 'jobs' : 'job';

    try {
      notify(`Attempting to retrigger/add ${jobTerm} via actions.json`, 'info');

      const pushIds = [...new Set(jobs.map(job => job.push_id))];
      const {
        data: taskIdMap,
        failureStatus,
      } = await PushModel.getDecisionTaskMap(pushIds);
      if (failureStatus) {
        notify(
          `Error getting Gecko Decision Task Ids: ${failureStatus}: ${taskIdMap}`,
        );
        return;
      }
      const uniquePerPushJobs = groupBy(jobs, job => job.push_id);

      for (const [key, value] of Object.entries(uniquePerPushJobs)) {
        const decisionTaskId = taskIdMap[key];

        TaskclusterModel.load(decisionTaskId).then(async results => {
          const actionTaskId = slugid();
          const addNewJobsTask = results.actions.find(
            action => action.name === 'add-new-jobs',
          );

          await TaskclusterModel.submit({
            action: addNewJobsTask,
            actionTaskId,
            decisionTaskId,
            taskId: null,
            task: null,
            input: { tasks: value.map(job => job.job_type_name) },
            staticActionVariables: results.staticActionVariables,
          })
            .then(() =>
              notify(
                `Request sent to retrigger/add new jobs via actions.json (${actionTaskId})`,
              ),
            )
            .catch(error => {
              notify(`Retrigger failed: ${error}`, 'danger', { sticky: true });
            });
        });<commit_msg>Bug 1521032: Use retrigger-multiple action to retrigger jobs. (#4887)<commit_after>import { slugid } from 'taskcluster-client-web';
import groupBy from 'lodash/groupBy';

import { thPlatformMap } from '../helpers/constants';
import { createQueryParams } from '../helpers/url';
import { formatTaskclusterError } from '../helpers/errorMessage';
import { getProjectUrl } from '../helpers/location';

import PushModel from './push';
import TaskclusterModel from './taskcluster';

const uri = getProjectUrl('/jobs/');

// JobModel is the js counterpart of job
export default class JobModel {
  constructor(props) {
    Object.assign(this, props);
  }

  getTitle() {
    // we want to join the group and type information together
    // so we can search for it as one token (useful when
    // we want to do a search on something like `fxup-esr(`)
    let symbolInfo = this.job_group_symbol === '?' ? '' : this.job_group_symbol;
    symbolInfo += `(${this.job_type_symbol})`;

    return [
      thPlatformMap[this.platform] || this.platform,
      this.platform_option,
      this.job_group_name === 'unknown' ? undefined : this.job_group_name,
      this.job_type_name,
      symbolInfo,
    ]
      .filter(item => typeof item !== 'undefined')
      .join(' ');
  }

  getSearchStr() {
    return [
      this.getTitle(),
      this.ref_data_name,
      this.signature !== this.ref_data_name ? this.signature : undefined,
    ]
      .filter(item => typeof item !== 'undefined')
      .join(' ');
  }

  static getList(repoName, options, config) {
    // a static method to retrieve a list of JobModel
    config = config || {};
    const fetch_all = config.fetch_all || false;
    // The `uri` config allows to fetch a list of jobs from an arbitrary
    // endpoint e.g. the similar jobs endpoint. It defaults to the job
    // list endpoint.
    const jobUri = config.uri || uri;

    return fetch(`${jobUri}${options ? createQueryParams(options) : ''}`).then(
      async resp => {
        if (resp.ok) {
          const data = await resp.json();
          let itemList;
          let nextPagesJobs = [];

          // if the number of elements returned equals the page size, fetch the next pages
          if (fetch_all && data.results.length === data.meta.count) {
            const count = parseInt(data.meta.count, 10);
            const offset = parseInt(data.meta.offset, 10) + count;
            const newOptions = { ...options, offset, count };

            nextPagesJobs = await JobModel.getList(
              repoName,
              newOptions,
              config,
            );
          }
          if ('job_property_names' in data) {
            // the results came as list of fields
            // we need to convert them to objects
            itemList = data.results.map(
              elem =>
                new JobModel(
                  data.job_property_names.reduce(
                    (prev, prop, i) => ({ ...prev, [prop]: elem[i] }),
                    {},
                  ),
                ),
            );
          } else {
            itemList = data.results.map(job_obj => new JobModel(job_obj));
          }
          return [...itemList, ...nextPagesJobs];
        }
        const text = await resp.text();
        throw Error(text);
      },
    );
  }

  static get(repoName, pk, signal) {
    // a static method to retrieve a single instance of JobModel
    return fetch(`${uri}${pk}/`, { signal }).then(async response => {
      if (response.ok) {
        const job = await response.json();
        return new JobModel(job);
      }
      const text = await response.text();
      throw Error(`Loading job with id ${pk} : ${text}`);
    });
  }

  static getSimilarJobs(repoName, pk, options, config) {
    config = config || {};
    // The similar jobs endpoints returns the same type of objects as
    // the job list endpoint, so let's reuse the getList method logic.
    config.uri = `${uri}${pk}/similar_jobs/`;
    return JobModel.getList(repoName, options, config);
  }

  static async retrigger(jobs, repoName, notify) {
    const jobTerm = jobs.length > 1 ? 'jobs' : 'job';

    try {
      notify(`Attempting to retrigger/add ${jobTerm} via actions.json`, 'info');

      const pushIds = [...new Set(jobs.map(job => job.push_id))];
      const {
        data: taskIdMap,
        failureStatus,
      } = await PushModel.getDecisionTaskMap(pushIds);
      if (failureStatus) {
        notify(
          `Error getting Gecko Decision Task Ids: ${failureStatus}: ${taskIdMap}`,
        );
        return;
      }
      const uniquePerPushJobs = groupBy(jobs, job => job.push_id);

      for (const [key, value] of Object.entries(uniquePerPushJobs)) {
        const decisionTaskId = taskIdMap[key];

        TaskclusterModel.load(decisionTaskId).then(async results => {
          const actionTaskId = slugid();
          const taskLabels = value.map(job => job.job_type_name);

          let retriggerAction = results.actions.find(
            action => action.name === 'retrigger-multiple',
          );
          let actionInput = {
            requests: [{ tasks: taskLabels }],
          };
          if (!retriggerAction) {
            // The `retrigger-multiple` action as introduced in Bug 1521032, to all the action
            // to control whether new task are created, or existing ones re-run. We fall back
            // to `add-new-jobs` to support pushing old revision to try, where the duplicating
            // the release tasks impacted is unlikely to cause problems.
            retriggerAction = results.actions.find(
              action => action.name === 'add-new-jobs',
            );
            actionInput = {
              tasks: taskLabels,
            };
          }

          await TaskclusterModel.submit({
            action: retriggerAction,
            actionTaskId,
            decisionTaskId,
            taskId: null,
            task: null,
            input: actionInput,
            staticActionVariables: results.staticActionVariables,
          })
            .then(() =>
              notify(
                `Request sent to retrigger/add new jobs via actions.json (${actionTaskId})`,
              ),
            )
            .catch(error => {
              notify(`Retrigger failed: ${error}`, 'danger', { sticky: true });
            });
        });
{'fraction_non_alphanumeric': 0.12231543624161074, 'fraction_numerical': 0.002348993288590604, 'mean_word_length': 2.0341053703232377, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 34, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7083229', 'n_tokens_mistral': 3355, 'n_tokens_neox': 3271, 'n_words': 946}