xref: /arm-trusted-firmware/tools/conventional-changelog-tf-a/index.js (revision 91f16700b400a8c0651d24a598fc48ee2997a0d7)
1*91f16700Schasinglulu/*
2*91f16700Schasinglulu * Copyright (c) 2021-2023, Arm Limited. All rights reserved.
3*91f16700Schasinglulu *
4*91f16700Schasinglulu * SPDX-License-Identifier: BSD-3-Clause
5*91f16700Schasinglulu */
6*91f16700Schasinglulu
7*91f16700Schasinglulu/* eslint-env es6 */
8*91f16700Schasinglulu
9*91f16700Schasinglulu"use strict";
10*91f16700Schasinglulu
11*91f16700Schasingluluconst Handlebars = require("handlebars");
12*91f16700Schasingluluconst Q = require("q");
13*91f16700Schasingluluconst _ = require("lodash");
14*91f16700Schasinglulu
15*91f16700Schasingluluconst ccConventionalChangelog = require("conventional-changelog-conventionalcommits/conventional-changelog");
16*91f16700Schasingluluconst ccParserOpts = require("conventional-changelog-conventionalcommits/parser-opts");
17*91f16700Schasingluluconst ccRecommendedBumpOpts = require("conventional-changelog-conventionalcommits/conventional-recommended-bump");
18*91f16700Schasingluluconst ccWriterOpts = require("conventional-changelog-conventionalcommits/writer-opts");
19*91f16700Schasinglulu
20*91f16700Schasingluluconst execa = require("execa");
21*91f16700Schasinglulu
22*91f16700Schasingluluconst readFileSync = require("fs").readFileSync;
23*91f16700Schasingluluconst resolve = require("path").resolve;
24*91f16700Schasinglulu
25*91f16700Schasinglulu/*
26*91f16700Schasinglulu * Register a Handlebars helper that lets us generate Markdown lists that can support multi-line
27*91f16700Schasinglulu * strings. This is driven by inconsistent formatting of breaking changes, which may be multiple
28*91f16700Schasinglulu * lines long and can terminate the list early unintentionally.
29*91f16700Schasinglulu */
30*91f16700SchasingluluHandlebars.registerHelper("tf-a-mdlist", function (indent, options) {
31*91f16700Schasinglulu    const spaces = new Array(indent + 1).join(" ");
32*91f16700Schasinglulu    const first = spaces + "- ";
33*91f16700Schasinglulu    const nth = spaces + "  ";
34*91f16700Schasinglulu
35*91f16700Schasinglulu    return first + options.fn(this).replace(/\n(?!\s*\n)/gm, `\n${nth}`).trim() + "\n";
36*91f16700Schasinglulu});
37*91f16700Schasinglulu
38*91f16700Schasinglulu/*
39*91f16700Schasinglulu * Register a Handlebars helper that concatenates multiple variables. We use this to generate the
40*91f16700Schasinglulu * title for the section partials.
41*91f16700Schasinglulu */
42*91f16700SchasingluluHandlebars.registerHelper("tf-a-concat", function () {
43*91f16700Schasinglulu    let argv = Array.prototype.slice.call(arguments, 0);
44*91f16700Schasinglulu
45*91f16700Schasinglulu    argv.pop();
46*91f16700Schasinglulu
47*91f16700Schasinglulu    return argv.join("");
48*91f16700Schasinglulu});
49*91f16700Schasinglulu
50*91f16700Schasinglulufunction writerOpts(config) {
51*91f16700Schasinglulu    /*
52*91f16700Schasinglulu     * Flatten the configuration's sections list. This helps us iterate over all of the sections
53*91f16700Schasinglulu     * when we don't care about the hierarchy.
54*91f16700Schasinglulu     */
55*91f16700Schasinglulu
56*91f16700Schasinglulu    const flattenSections = function (sections) {
57*91f16700Schasinglulu        return sections.flatMap(section => {
58*91f16700Schasinglulu            const subsections = flattenSections(section.sections || []);
59*91f16700Schasinglulu
60*91f16700Schasinglulu            return [section].concat(subsections);
61*91f16700Schasinglulu        })
62*91f16700Schasinglulu    };
63*91f16700Schasinglulu
64*91f16700Schasinglulu    const flattenedSections = flattenSections(config.sections);
65*91f16700Schasinglulu
66*91f16700Schasinglulu    /*
67*91f16700Schasinglulu     * Register a helper to return a restructured version of the note groups that includes notes
68*91f16700Schasinglulu     * categorized by their section.
69*91f16700Schasinglulu     */
70*91f16700Schasinglulu    Handlebars.registerHelper("tf-a-notes", function (noteGroups, options) {
71*91f16700Schasinglulu        const generateTemplateData = function (sections, notes) {
72*91f16700Schasinglulu            return (sections || []).flatMap(section => {
73*91f16700Schasinglulu                const templateData = {
74*91f16700Schasinglulu                    title: section.title,
75*91f16700Schasinglulu                    sections: generateTemplateData(section.sections, notes),
76*91f16700Schasinglulu                    notes: notes.filter(note => section.scopes?.includes(note.commit.scope)),
77*91f16700Schasinglulu                };
78*91f16700Schasinglulu
79*91f16700Schasinglulu                /*
80*91f16700Schasinglulu                 * Don't return a section if it contains no notes and no sub-sections.
81*91f16700Schasinglulu                 */
82*91f16700Schasinglulu                if ((templateData.sections.length == 0) && (templateData.notes.length == 0)) {
83*91f16700Schasinglulu                    return [];
84*91f16700Schasinglulu                }
85*91f16700Schasinglulu
86*91f16700Schasinglulu                return [templateData];
87*91f16700Schasinglulu            });
88*91f16700Schasinglulu        };
89*91f16700Schasinglulu
90*91f16700Schasinglulu        return noteGroups.map(noteGroup => {
91*91f16700Schasinglulu            return {
92*91f16700Schasinglulu                title: noteGroup.title,
93*91f16700Schasinglulu                sections: generateTemplateData(config.sections, noteGroup.notes),
94*91f16700Schasinglulu                notes: noteGroup.notes.filter(note =>
95*91f16700Schasinglulu                    !flattenedSections.some(section => section.scopes?.includes(note.commit.scope))),
96*91f16700Schasinglulu            };
97*91f16700Schasinglulu        });
98*91f16700Schasinglulu    });
99*91f16700Schasinglulu
100*91f16700Schasinglulu    /*
101*91f16700Schasinglulu     * Register a helper to return a restructured version of the commit groups that includes commits
102*91f16700Schasinglulu     * categorized by their section.
103*91f16700Schasinglulu     */
104*91f16700Schasinglulu    Handlebars.registerHelper("tf-a-commits", function (commitGroups, options) {
105*91f16700Schasinglulu        const generateTemplateData = function (sections, commits) {
106*91f16700Schasinglulu            return (sections || []).flatMap(section => {
107*91f16700Schasinglulu                const templateData = {
108*91f16700Schasinglulu                    title: section.title,
109*91f16700Schasinglulu                    sections: generateTemplateData(section.sections, commits),
110*91f16700Schasinglulu                    commits: commits.filter(commit => section.scopes?.includes(commit.scope)),
111*91f16700Schasinglulu                };
112*91f16700Schasinglulu
113*91f16700Schasinglulu                /*
114*91f16700Schasinglulu                 * Don't return a section if it contains no notes and no sub-sections.
115*91f16700Schasinglulu                 */
116*91f16700Schasinglulu                if ((templateData.sections.length == 0) && (templateData.commits.length == 0)) {
117*91f16700Schasinglulu                    return [];
118*91f16700Schasinglulu                }
119*91f16700Schasinglulu
120*91f16700Schasinglulu                return [templateData];
121*91f16700Schasinglulu            });
122*91f16700Schasinglulu        };
123*91f16700Schasinglulu
124*91f16700Schasinglulu        return commitGroups.map(commitGroup => {
125*91f16700Schasinglulu            return {
126*91f16700Schasinglulu                title: commitGroup.title,
127*91f16700Schasinglulu                sections: generateTemplateData(config.sections, commitGroup.commits),
128*91f16700Schasinglulu                commits: commitGroup.commits.filter(commit =>
129*91f16700Schasinglulu                    !flattenedSections.some(section => section.scopes?.includes(commit.scope))),
130*91f16700Schasinglulu            };
131*91f16700Schasinglulu        });
132*91f16700Schasinglulu    });
133*91f16700Schasinglulu
134*91f16700Schasinglulu    const writerOpts = ccWriterOpts(config)
135*91f16700Schasinglulu        .then(writerOpts => {
136*91f16700Schasinglulu            const ccWriterOptsTransform = writerOpts.transform;
137*91f16700Schasinglulu
138*91f16700Schasinglulu            /*
139*91f16700Schasinglulu             * These configuration properties can't be injected directly into the template because
140*91f16700Schasinglulu             * they themselves are templates. Instead, we register them as partials, which allows
141*91f16700Schasinglulu             * them to be evaluated as part of the templates they're used in.
142*91f16700Schasinglulu             */
143*91f16700Schasinglulu            Handlebars.registerPartial("commitUrl", config.commitUrlFormat);
144*91f16700Schasinglulu            Handlebars.registerPartial("compareUrl", config.compareUrlFormat);
145*91f16700Schasinglulu            Handlebars.registerPartial("issueUrl", config.issueUrlFormat);
146*91f16700Schasinglulu
147*91f16700Schasinglulu            /*
148*91f16700Schasinglulu             * Register the partials that allow us to recursively create changelog sections.
149*91f16700Schasinglulu             */
150*91f16700Schasinglulu
151*91f16700Schasinglulu            const notePartial = readFileSync(resolve(__dirname, "./templates/note.hbs"), "utf-8");
152*91f16700Schasinglulu            const noteSectionPartial = readFileSync(resolve(__dirname, "./templates/note-section.hbs"), "utf-8");
153*91f16700Schasinglulu            const commitSectionPartial = readFileSync(resolve(__dirname, "./templates/commit-section.hbs"), "utf-8");
154*91f16700Schasinglulu
155*91f16700Schasinglulu            Handlebars.registerPartial("tf-a-note", notePartial);
156*91f16700Schasinglulu            Handlebars.registerPartial("tf-a-note-section", noteSectionPartial);
157*91f16700Schasinglulu            Handlebars.registerPartial("tf-a-commit-section", commitSectionPartial);
158*91f16700Schasinglulu
159*91f16700Schasinglulu            /*
160*91f16700Schasinglulu             * Override the base templates so that we can generate a changelog that looks at least
161*91f16700Schasinglulu             * similar to the pre-Conventional Commits TF-A changelog.
162*91f16700Schasinglulu             */
163*91f16700Schasinglulu            writerOpts.mainTemplate = readFileSync(resolve(__dirname, "./templates/template.hbs"), "utf-8");
164*91f16700Schasinglulu            writerOpts.headerPartial = readFileSync(resolve(__dirname, "./templates/header.hbs"), "utf-8");
165*91f16700Schasinglulu            writerOpts.commitPartial = readFileSync(resolve(__dirname, "./templates/commit.hbs"), "utf-8");
166*91f16700Schasinglulu            writerOpts.footerPartial = readFileSync(resolve(__dirname, "./templates/footer.hbs"), "utf-8");
167*91f16700Schasinglulu
168*91f16700Schasinglulu            writerOpts.transform = function (commit, context) {
169*91f16700Schasinglulu                /*
170*91f16700Schasinglulu                 * Feedback on the generated changelog has shown that having build system changes
171*91f16700Schasinglulu                 * appear at the top of a section throws some people off. We make an exception for
172*91f16700Schasinglulu                 * scopeless `build`-type changes and treat them as though they actually have the
173*91f16700Schasinglulu                 * `build` scope.
174*91f16700Schasinglulu                 */
175*91f16700Schasinglulu
176*91f16700Schasinglulu                if ((commit.type === "build") && (commit.scope == null)) {
177*91f16700Schasinglulu                    commit.scope = "build";
178*91f16700Schasinglulu                }
179*91f16700Schasinglulu
180*91f16700Schasinglulu                /*
181*91f16700Schasinglulu                 * Fix up commit trailers, which for some reason are not correctly recognized and
182*91f16700Schasinglulu                 * end up showing up in the breaking changes.
183*91f16700Schasinglulu                 */
184*91f16700Schasinglulu
185*91f16700Schasinglulu                commit.notes.forEach(note => {
186*91f16700Schasinglulu                    const trailers = execa.sync("git", ["interpret-trailers", "--parse"], {
187*91f16700Schasinglulu                        input: note.text
188*91f16700Schasinglulu                    }).stdout;
189*91f16700Schasinglulu
190*91f16700Schasinglulu                    note.text = note.text.replace(trailers, "").trim();
191*91f16700Schasinglulu                });
192*91f16700Schasinglulu
193*91f16700Schasinglulu                return ccWriterOptsTransform(commit, context);
194*91f16700Schasinglulu            };
195*91f16700Schasinglulu
196*91f16700Schasinglulu            return writerOpts;
197*91f16700Schasinglulu        });
198*91f16700Schasinglulu
199*91f16700Schasinglulu    return writerOpts;
200*91f16700Schasinglulu}
201*91f16700Schasinglulu
202*91f16700Schasinglulumodule.exports = function (parameter) {
203*91f16700Schasinglulu    const config = parameter || {};
204*91f16700Schasinglulu
205*91f16700Schasinglulu    return Q.all([
206*91f16700Schasinglulu        ccConventionalChangelog(config),
207*91f16700Schasinglulu        ccParserOpts(config),
208*91f16700Schasinglulu        ccRecommendedBumpOpts(config),
209*91f16700Schasinglulu        writerOpts(config)
210*91f16700Schasinglulu    ]).spread((
211*91f16700Schasinglulu        conventionalChangelog,
212*91f16700Schasinglulu        parserOpts,
213*91f16700Schasinglulu        recommendedBumpOpts,
214*91f16700Schasinglulu        writerOpts
215*91f16700Schasinglulu    ) => {
216*91f16700Schasinglulu        if (_.isFunction(parameter)) {
217*91f16700Schasinglulu            return parameter(null, {
218*91f16700Schasinglulu                gitRawCommitsOpts: { noMerges: null },
219*91f16700Schasinglulu                conventionalChangelog,
220*91f16700Schasinglulu                parserOpts,
221*91f16700Schasinglulu                recommendedBumpOpts,
222*91f16700Schasinglulu                writerOpts
223*91f16700Schasinglulu            });
224*91f16700Schasinglulu        } else {
225*91f16700Schasinglulu            return {
226*91f16700Schasinglulu                conventionalChangelog,
227*91f16700Schasinglulu                parserOpts,
228*91f16700Schasinglulu                recommendedBumpOpts,
229*91f16700Schasinglulu                writerOpts
230*91f16700Schasinglulu            };
231*91f16700Schasinglulu        }
232*91f16700Schasinglulu    });
233*91f16700Schasinglulu};
234