Skip to content

Commit

Permalink
feat: parse and render release notes
Browse files Browse the repository at this point in the history
  • Loading branch information
iowillhoit committed Nov 15, 2021
1 parent 20e21bf commit f5e3398
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 21 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"@salesforce/core": "^2.25.1",
"@salesforce/kit": "^1.5.17",
"got": "^11.8.2",
"marked": "^4.0.0",
"marked": "^4.0.1",
"marked-terminal": "^4.2.0",
"semver": "^7.3.5",
"sinon-chai": "^3.7.0",
Expand Down
2 changes: 1 addition & 1 deletion src/commands/info/releasenotes/display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { Messages } from '@salesforce/core';
import { getInfoConfig, InfoConfig } from '../../../shared/getInfoConfig';
import { getReleaseNotes } from '../../../shared/getReleaseNotes';
import { getDistTagVersion } from '../../../shared/getDistTagVersion';
import { parseReleaseNotes } from '../../../shared/parse-release-notes';
import { parseReleaseNotes } from '../../../shared/parseReleaseNotes';

// Initialize Messages with the current plugin directory
Messages.importMessagesDirectory(__dirname);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@

import { marked } from 'marked';

export function parseReleaseNotes(notes: string, version: string, baseUrl?: string): marked.Token[] {
const parseReleaseNotes = (notes: string, version: string, baseUrl: string): marked.Token[] => {
let found = false;

const parsed = marked.lexer(notes);

const tokens = parsed.filter((token) => {
// TODO: Make header depth a setting in oclif.info?
// TODO: Could make header depth (2) a setting in oclif.info.releasenotes
if (token.type === 'heading' && token.depth === 2) {
if (token.text.includes(version)) {
found = true;
Expand All @@ -33,6 +33,7 @@ export function parseReleaseNotes(notes: string, version: string, baseUrl?: stri

const fixRelativeLinks = (token: marked.Token): void => {
// If link is relative, add the baseurl. https://regex101.com/r/h802kJ/1
// FWIW: 'marked' does have a 'baseURL' option, but the 'marked-terminal' renderer does not honor it
if (token.type === 'link' && !token.href.match(/(?:[a-z][a-z0-9+.-]*:|\/\/)/gi)) {
token.href = `${baseUrl}/${token.href}`;
}
Expand All @@ -41,4 +42,6 @@ export function parseReleaseNotes(notes: string, version: string, baseUrl?: stri
marked.walkTokens(tokens, fixRelativeLinks);

return tokens;
}
};

export { parseReleaseNotes };
55 changes: 43 additions & 12 deletions test/commands/info/releasenotes/display.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
import { expect, use as chaiUse } from 'chai';
import * as Sinon from 'sinon';
import * as SinonChai from 'sinon-chai';
import { fromStub, stubInterface, stubMethod } from '@salesforce/ts-sinon';
import { fromStub, stubInterface, stubMethod, spyMethod } from '@salesforce/ts-sinon';
import { IConfig } from '@oclif/config';
import { UX } from '@salesforce/command';
import { marked } from 'marked';
import * as getInfoConfig from '../../../../src/shared/getInfoConfig';
import * as getReleaseNotes from '../../../../src/shared/getReleaseNotes';
import * as getDistTagVersion from '../../../../src/shared/getDistTagVersion';
import * as parseReleaseNotes from '../../../../src/shared/parseReleaseNotes';
import Display from '../../../../src/commands/info/releasenotes/display';

chaiUse(SinonChai);
Expand All @@ -27,6 +29,8 @@ describe('info:releasenotes:display', () => {
let getInfoConfigStub: Sinon.SinonStub;
let getReleaseNotesStub: Sinon.SinonStub;
let getDistTagVersionStub: Sinon.SinonStub;
let parseReleaseNotesSpy: Sinon.SinonSpy;
let markedParserSpy: Sinon.SinonSpy;

const oclifConfigStub = fromStub(stubInterface<IConfig>(sandbox));

Expand All @@ -45,13 +49,14 @@ describe('info:releasenotes:display', () => {

return cmd.runIt();
};

let suppressEnvVarBackup;

beforeEach(() => {
mockInfoConfig = {
releasenotes: {
distTagUrl: 'https://registry.npmjs.org/-/package/sfdx-cli/dist-tags',
releaseNotesPath: 'https://raw.githubusercontent.com/forcedotcom/cli/main/releasenotes/sfdx',
releaseNotesPath: 'https://github.com/forcedotcom/cli/tree/main/releasenotes/sfdx',
releaseNotesFilename: 'README.md',
},
};
Expand All @@ -62,26 +67,19 @@ describe('info:releasenotes:display', () => {
suppressEnvVarBackup = process.env.PLUGIN_INFO_HIDE_RELEASE_NOTES;

getInfoConfigStub = stubMethod(sandbox, getInfoConfig, 'getInfoConfig').returns(mockInfoConfig);
getReleaseNotesStub = stubMethod(sandbox, getReleaseNotes, 'getReleaseNotes').returns('release-notes');
getReleaseNotesStub = stubMethod(sandbox, getReleaseNotes, 'getReleaseNotes').returns('## Release notes for 3.3.3');
getDistTagVersionStub = stubMethod(sandbox, getDistTagVersion, 'getDistTagVersion').returns('1.2.3');
parseReleaseNotesSpy = spyMethod(sandbox, parseReleaseNotes, 'parseReleaseNotes');
markedParserSpy = spyMethod(sandbox, marked, 'parser');
});

afterEach(() => {
getInfoConfigStub.restore();
getReleaseNotesStub.restore();
getDistTagVersionStub.restore();
sandbox.restore();

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
process.env.PLUGIN_INFO_HIDE_RELEASE_NOTES = suppressEnvVarBackup;
});

it('logs release notes', async () => {
await runDisplayCmd([]);

expect(uxLogStub.args[0][0]).to.equal('release-notes');
});

it('allows you to suppress release notes output with env var', async () => {
process.env.PLUGIN_INFO_HIDE_RELEASE_NOTES = 'true';

Expand Down Expand Up @@ -156,4 +154,37 @@ describe('info:releasenotes:display', () => {

expect(uxWarnStub.args[0][0]).to.contain('release notes error');
});

it('parseReleaseNotes is called with the correct args', async () => {
await runDisplayCmd([]);

expect(parseReleaseNotesSpy.args[0]).to.deep.equal([
'## Release notes for 3.3.3',
'3.3.3',
mockInfoConfig.releasenotes.releaseNotesPath,
]);
});

it('parser is called with tokens', async () => {
await runDisplayCmd([]);

const tokens = parseReleaseNotesSpy.returnValues[0] as marked.Token;

expect(markedParserSpy.calledOnce).to.be.true;
expect(markedParserSpy.args[0][0]).to.deep.equal(tokens);
});

it('logs markdown on the command line', async () => {
await runDisplayCmd([]);

expect(uxLogStub.args[0][0]).to.contain('## Release notes for 3.3.3');
});

it('logs warning if parsing fails', async () => {
await runDisplayCmd(['-v', '4.5.6']);

expect(uxWarnStub.args[0][0]).to.contain(
`Version '4.5.6' was not found. You can view release notes online at: ${mockInfoConfig.releasenotes.releaseNotesPath}`
);
});
});
70 changes: 70 additions & 0 deletions test/fixtures/notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
## 7.125.0 (Nov 4, 2021)

- testing a nested

- list
- with items
- does this work
- `source-foo-bar-wow`
- `source_foo_bar_wow`
- `source:foo:bar:wow`
- huzzah

- NEW: We've greatly improved the source tracking commands, and we'd love you to try out the beta versions before we make them generally available.

We're doing something different this time: rather than provide the beta commands in a separate plug-in, we're including them right in this release. The beta command names are similar to the existing ones, but are in the `force:source:beta` topic:

- `force:source:beta:pull`
- `force:source:beta:push`
- `force:source:beta:status`
- `force:source:beta:tracking:clear`
- `force:source:beta:tracking:reset`

The existing commands, such as `force:source:pull`, continue to work as before. Check out [Source Tracking Beta](https://github.com/forcedotcom/cli/issues/1258) for additional details and gotchas.

## 7.123.0 (Oct 21, 2021)

- FIX: The `force:source:deploy|retrieve|convert|delete` commands support the `EclairGeoData` metadata type again.

- FIX: The `force:org:display --json` command displays in the JSON output the security warning about exposing sensitive information, consistent with the other commands. ([GitHub issue #1229](https://github.com/forcedotcom/cli/issues/1229))

- FIX: The `force:org:open` command correctly opens sandboxes in the `.mil` domain. Previously the command tried to open a different domain.

## 7.122.1 (Oct 14, 2021)

- NEW: The `force:source:deploy|retrieve|convert|delete` commands support these Slack-related metadata types that are new in the Winter '22 Salesforce release: `ViewDefinition` and `SlackApp`.

- FIX: The `force:source:deploy|retrieve|convert|delete` commands support the `SharingGuestRule` metadata type again. ([GitHub issue #1199](https://github.com/forcedotcom/cli/issues/1199))

- FIX: The `force:source:delete` command no longer fails when you try to delete metadata types that are in the org, but not in your local project directory. Previously you'd get the error `Entity of type MD-Type named MD-Type-Name cannot be found`. ([GitHub issue #1203](https://github.com/forcedotcom/cli/issues/1203))

## 7.121.8 (Oct 7, 2021)

- CHANGE: As we announced on [March 18, 2021](./README.md#5140-march-18-2021---cli-7920), the `--json` output of the `force:org:list` command no longer returns the property `connectedStatus` for scratch orgs. We've also removed the warning.
- Testing [relative link](./test.md) in nested list
- FIX: When you delete a scratch org with the `force:org:delete` command, we now ensure that the associated Dev Hub org always deletes the corresponding record from the ActiveScratchOrg object. Previously, in certain circumstances, the record wasn't deleted, which could cause you to incorrectly exceed over your scratch org limit. ([GitHub issue #1155](https://github.com/forcedotcom/cli/issues/1155))
- FIX: The `force:source:convert` command correctly converts the `CustomFieldTranslation` metadata type.

## 7.120.0 (Sept 30, 2021)

- NEW: Some commands (`force:org:open`, `force:org:display`, `force:user:display`, `force:source:open`) display tokens and URLs with tokens embedded in them. They now include a warning about the risks of sharing that sensitive information.

- NEW: We've added a warning and additional information to explain why the command `force:source:retrieve -m CustomField` succeeds but returns no results, even when your org contains custom fields. This behavior is expected because the Metadata API never returns results if you retrieve the `CustomField` type on its own without also retrieving `CustomObject`. If you want to retrieve all your custom fields, try this command instead: `force:source:retrieve -m CustomField,CustomObject`.

You get the same behavior if you run `force:source:retrieve -x manifest` and your manifest file contains a `CustomField` entry with an `*` to retrieve all custom fields, but no `CustomObject` entry. Check out the new warning for hints to actually retrieve your custom fields.

- NEW: Specify the level of deployment tests to run when you delete metadata source files with the new `--testlevel` parameter of `force:source:delete`. The new parameter works the same as the `--testlevel` parameter of `force:source:deploy`, although the list of valid values is shorter: `NoTestRun`, `RunLocalTests`, and `RunAllTestsInOrg`. See the [CLI Reference guide](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference_force_source.htm#cli_reference_force_source_deploy) for more information about each value.

For example, to run all org tests when you delete the MyMetadataType type from both your local project and the org, run this command:

`sfdx force:source:delete --metadata MyMetadataType --testlevel RunAllTestsInOrg`

As a result of this new feature, [GitHub issue #971](https://github.com/forcedotcom/cli/issues/971) is fixed.

- CHANGE: As we [warned last year](./v50.md#5020-october-22-2020---cli-7771), the `force:source:*` commands no longer support the old format of the `.forceignore` file. When parsing the `.forceignore` file, the commands now always use the same rules and patterns as [git uses with the `.gitignore` file](https://git-scm.com/docs/gitignore).

- FIX: The `force:source:deploy|retrieve|convert` commands now support the Reports, Dashboards, Documents, and EmailTemplates metadata types when they're nested in directories of more than one level. (GitHub issues [#1112](https://github.com/forcedotcom/cli/issues/1112) and [#1173](https://github.com/forcedotcom/cli/issues/1173))

- FIX: The `force:source:deploy` command provides more failure details in situations where it previously reported the un-helpful `Deploy Failed`

- FIX: The `force:org:create` command now uses only polling, and not the Streaming API, to listen for updates about the scratch org creation. As a result, you no longer get the error `Socket timeout occurred while listening for results` if the scratch org creation takes a long time. ([GitHub issue #1149](https://github.com/forcedotcom/cli/issues/1149))
67 changes: 67 additions & 0 deletions test/shared/parseReleaseNotes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright (c) 2021, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import { expect, use as chaiUse } from 'chai';
import * as Sinon from 'sinon';
import * as SinonChai from 'sinon-chai';
import { spyMethod } from '@salesforce/ts-sinon';
import { fs } from '@salesforce/core';
import { marked } from 'marked';
import { parseReleaseNotes } from '../../src/shared/parseReleaseNotes';

chaiUse(SinonChai);

describe('parseReleaseNotes tests', () => {
const sandbox = Sinon.createSandbox();
const notes = fs.readFileSync(`${__dirname}/../fixtures/notes.md`, 'utf8');
const baseUrl = 'https://github.com/forcedotcom/cli/tree/main/releasenotes/sfdx';

let lexerSpy: Sinon.SinonSpy;

beforeEach(() => {
lexerSpy = spyMethod(sandbox, marked, 'lexer');
});

afterEach(() => {
sandbox.restore();
});

it('calls lexer with raw release notes', async () => {
parseReleaseNotes(notes, '7.121.8', baseUrl);

expect(lexerSpy.called).to.be.true;
expect(lexerSpy.args[0][0]).to.deep.equal(notes);
});

it('filters out correct version from tokens', async () => {
const tokens = parseReleaseNotes(notes, '7.121.8', baseUrl);

const results = JSON.stringify(tokens, null, ' ');

expect(tokens[0].raw).to.include('7.121.8');
expect(results).to.include('7.121.8');
expect(results).to.not.include('7.123.0');
expect(results).to.not.include('7.122.1');
expect(results).to.not.include('7.120.0');
});

it('throws error if version is not found', async () => {
try {
parseReleaseNotes(notes, '1.2.3', baseUrl);
} catch (err) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(err.message).to.equal(`Version '1.2.3' was not found. You can view release notes online at: ${baseUrl}`);
}
});

it('fixes relative links in releasenotes', async () => {
const tokens = parseReleaseNotes(notes, '7.121.8', baseUrl);

const results = JSON.stringify(tokens, null, ' ');
expect(results).to.include(`${baseUrl}/./test.md`);
});
});
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4575,10 +4575,10 @@ marked@^1.1.1:
resolved "https://registry.npmjs.org/marked/-/marked-1.2.8.tgz#5008ece15cfa43e653e85845f3525af4beb6bdd4"
integrity sha512-lzmFjGnzWHkmbk85q/ILZjFoHHJIQGF+SxGEfIdGk/XhiTPhqGs37gbru6Kkd48diJnEyYwnG67nru0Z2gQtuQ==

marked@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.0.tgz#fd4ff16f6b99fbe6beb729f2077ea717d0ec4edb"
integrity sha512-K3C1JvtiXuXVLoxDQEJP4NMLBuThlTkthgUOCzqLghIpHfis1DIZZfPI3o4UgfFpQ0d+JvTql2h+szR9jQ1p1w==
marked@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.1.tgz#eaec1f18ab3d5a071697cebe45e0ae4b2216a8f5"
integrity sha512-L90F6VQdYJSL1WVaIGCbNASAWnPCyB/jGmvQ/KIk0ThYq0XuzRrWxhwjcHoYvIZlQHKD/C/2i7DAADFPgxV7Tw==

meow@^3.3.0:
version "3.7.0"
Expand Down

0 comments on commit f5e3398

Please sign in to comment.