Skip to content

Commit 25b518f

Browse files
scolladonchingor13
andauthored
feat: add Salesforce strategy (#1815)
Co-authored-by: Jeff Ching <chingor@google.com>
1 parent 6cc85db commit 25b518f

11 files changed

Lines changed: 391 additions & 2 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ Release Please automates releases for the following flavors of repositories:
150150
| `python` | [A Python repository, with a setup.py, setup.cfg, CHANGELOG.md](https://github.com/googleapis/python-storage) and optionally a pyproject.toml and a &lt;project&gt;/\_\_init\_\_.py |
151151
| `ruby` | A repository with a version.rb and a CHANGELOG.md |
152152
| `rust` | A Rust repository, with a Cargo.toml (either as a crate or workspace) and a CHANGELOG.md |
153+
| `salesforce` | A repository with a sfdx-project.json and a CHANGELOG.md |
153154
| `simple` | [A repository with a version.txt and a CHANGELOG.md](https://github.com/googleapis/gapic-generator) |
154155
| `terraform-module` | [A terraform module, with a version in the README.md, and a CHANGELOG.md](https://github.com/terraform-google-modules/terraform-google-project-factory) |
155156

__snapshots__/cli.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ Options:
4242
[choices: "dart", "dotnet-yoshi", "elixir", "expo", "go", "go-yoshi", "helm",
4343
"java", "java-backport", "java-bom", "java-lts", "java-yoshi",
4444
"java-yoshi-mono-repo", "krm-blueprint", "maven", "node", "ocaml", "php",
45-
"php-yoshi", "python", "ruby", "ruby-yoshi", "rust", "simple",
45+
"php-yoshi", "python", "ruby", "ruby-yoshi", "rust", "salesforce", "simple",
4646
"terraform-module"]
4747
--config-file where can the config file be found in the
4848
project? [default: "release-please-config.json"]
@@ -241,7 +241,7 @@ Options:
241241
[choices: "dart", "dotnet-yoshi", "elixir", "expo", "go", "go-yoshi", "helm",
242242
"java", "java-backport", "java-bom", "java-lts", "java-yoshi",
243243
"java-yoshi-mono-repo", "krm-blueprint", "maven", "node", "ocaml", "php",
244-
"php-yoshi", "python", "ruby", "ruby-yoshi", "rust", "simple",
244+
"php-yoshi", "python", "ruby", "ruby-yoshi", "rust", "salesforce", "simple",
245245
"terraform-module"]
246246
--config-file where can the config file be found in the
247247
project?

__snapshots__/sfdx-project-json.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
exports['SfdxProjectJson updateContent updates version in sfdx-project.json 1'] = `
2+
{
3+
"packageDirectories": [
4+
{
5+
"default": true,
6+
"versionNumber": "2.3.4.NEXT"
7+
}
8+
],
9+
"name": "salesfore-test-repo"
10+
}
11+
12+
`

docs/customizing.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Release Please automates releases for the following flavors of repositories:
2323
| `python` | [A Python repository, with a setup.py, setup.cfg, CHANGELOG.md](https://github.com/googleapis/python-storage) and optionally a pyproject.toml and a &lt;project&gt;/\_\_init\_\_.py |
2424
| `ruby` | A repository with a version.rb and a CHANGELOG.md |
2525
| `rust` | A Rust repository, with a Cargo.toml (either as a crate or workspace) and a CHANGELOG.md |
26+
| `salesforce` | A repository with a sfdx-project.json and a CHANGELOG.md |
2627
| `simple` | [A repository with a version.txt and a CHANGELOG.md](https://github.com/googleapis/gapic-generator) |
2728
| `terraform-module` | [A terraform module, with a version in the README.md, and a CHANGELOG.md](https://github.com/terraform-google-modules/terraform-google-project-factory) |
2829

src/factory.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {Python} from './strategies/python';
2525
import {Ruby} from './strategies/ruby';
2626
import {RubyYoshi} from './strategies/ruby-yoshi';
2727
import {Rust} from './strategies/rust';
28+
import {Salesforce} from './strategies/salesforce';
2829
import {Simple} from './strategies/simple';
2930
import {TerraformModule} from './strategies/terraform-module';
3031
import {Helm} from './strategies/helm';
@@ -99,6 +100,7 @@ const releasers: Record<string, ReleaseBuilder> = {
99100
ruby: options => new Ruby(options),
100101
'ruby-yoshi': options => new RubyYoshi(options),
101102
rust: options => new Rust(options),
103+
salesforce: options => new Salesforce(options),
102104
simple: options => new Simple(options),
103105
'terraform-module': options => new TerraformModule(options),
104106
helm: options => new Helm(options),

src/strategies/salesforce.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import {BaseStrategy, BuildUpdatesOptions} from './base';
16+
import {Update} from '../update';
17+
import {Changelog} from '../updaters/changelog';
18+
import {GitHubFileContents} from '@google-automations/git-file-utils';
19+
import {FileNotFoundError, MissingRequiredFileError} from '../errors';
20+
import {SfdxProjectJson} from '../updaters/salesforce/sfdx-project-json';
21+
22+
const sfdxProjectJsonFileName = 'sfdx-project.json';
23+
24+
export class Salesforce extends BaseStrategy {
25+
private sfdxProjectJsonContents?: GitHubFileContents;
26+
27+
protected async buildUpdates(
28+
options: BuildUpdatesOptions
29+
): Promise<Update[]> {
30+
const updates: Update[] = [];
31+
const version = options.newVersion;
32+
33+
updates.push({
34+
path: this.addPath(this.changelogPath),
35+
createIfMissing: true,
36+
updater: new Changelog({
37+
version,
38+
changelogEntry: options.changelogEntry,
39+
}),
40+
});
41+
42+
updates.push({
43+
path: this.addPath(sfdxProjectJsonFileName),
44+
createIfMissing: false,
45+
cachedFileContents: this.sfdxProjectJsonContents,
46+
updater: new SfdxProjectJson({
47+
version,
48+
}),
49+
});
50+
51+
return updates;
52+
}
53+
54+
async getDefaultPackageName(): Promise<string | undefined> {
55+
const pkgJsonContents = await this.getSfdxProjectJsonContents();
56+
const pkg = JSON.parse(pkgJsonContents.parsedContent);
57+
return pkg.name;
58+
}
59+
60+
protected async getSfdxProjectJsonContents(): Promise<GitHubFileContents> {
61+
if (!this.sfdxProjectJsonContents) {
62+
try {
63+
this.sfdxProjectJsonContents =
64+
await this.github.getFileContentsOnBranch(
65+
this.addPath(sfdxProjectJsonFileName),
66+
this.targetBranch
67+
);
68+
} catch (e) {
69+
if (e instanceof FileNotFoundError) {
70+
throw new MissingRequiredFileError(
71+
this.addPath(sfdxProjectJsonFileName),
72+
'salesforce',
73+
`${this.repository.owner}/${this.repository.repo}`
74+
);
75+
}
76+
throw e;
77+
}
78+
}
79+
return this.sfdxProjectJsonContents;
80+
}
81+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import {jsonStringify} from '../../util/json-stringify';
16+
import {logger as defaultLogger, Logger} from '../../util/logger';
17+
import {DefaultUpdater} from '../default';
18+
19+
export type PackageDirectory = {
20+
versionNumber: string;
21+
default: boolean;
22+
};
23+
export type SfdxProjectFile = {
24+
packageDirectories: PackageDirectory[];
25+
name: string;
26+
};
27+
28+
/**
29+
* This updates a Salesfore sfdx-project.json file's main version.
30+
*/
31+
export class SfdxProjectJson extends DefaultUpdater {
32+
/**
33+
* Given initial file contents, return updated contents.
34+
* @param {string} content The initial content
35+
* @returns {string} The updated content
36+
*/
37+
updateContent(content: string, logger: Logger = defaultLogger): string {
38+
const parsed = JSON.parse(content) as SfdxProjectFile;
39+
for (const packDir of parsed.packageDirectories) {
40+
if (packDir.default) {
41+
logger.info(
42+
`updating from ${packDir.versionNumber} to ${this.version}`
43+
);
44+
packDir.versionNumber = `${this.version.toString()}.NEXT`;
45+
}
46+
}
47+
return jsonStringify(parsed, content);
48+
}
49+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"packageDirectories": [
3+
{
4+
"default": true,
5+
"versionNumber": "1.0.0.NEXT"
6+
}
7+
],
8+
"name": "salesfore-test-repo"
9+
}

test/strategies/salesforce.ts

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import {describe, it, afterEach, beforeEach} from 'mocha';
16+
import {Salesforce} from '../../src/strategies/salesforce';
17+
import {
18+
buildMockConventionalCommit,
19+
buildGitHubFileContent,
20+
assertHasUpdate,
21+
} from '../helpers';
22+
import * as nock from 'nock';
23+
import * as sinon from 'sinon';
24+
import {GitHub} from '../../src/github';
25+
import {Version} from '../../src/version';
26+
import {TagName} from '../../src/util/tag-name';
27+
import {expect} from 'chai';
28+
import {Changelog} from '../../src/updaters/changelog';
29+
import {SfdxProjectJson} from '../../src/updaters/salesforce/sfdx-project-json';
30+
import * as assert from 'assert';
31+
import {MissingRequiredFileError, FileNotFoundError} from '../../src/errors';
32+
33+
nock.disableNetConnect();
34+
const sandbox = sinon.createSandbox();
35+
const fixturesPath = './test/fixtures/strategies/salesforce';
36+
37+
describe('Salesforce', () => {
38+
let github: GitHub;
39+
const commits = [
40+
...buildMockConventionalCommit(
41+
'fix(deps): update dependency com.google.cloud:google-cloud-storage to v1.120.0'
42+
),
43+
];
44+
beforeEach(async () => {
45+
github = await GitHub.create({
46+
owner: 'googleapis',
47+
repo: 'salesforce-test-repo',
48+
defaultBranch: 'main',
49+
});
50+
});
51+
afterEach(() => {
52+
sandbox.restore();
53+
});
54+
describe('buildReleasePullRequest', () => {
55+
it('returns release PR changes with defaultInitialVersion', async () => {
56+
const expectedVersion = '1.0.0';
57+
const strategy = new Salesforce({
58+
targetBranch: 'main',
59+
github,
60+
component: 'google-cloud-automl',
61+
packageName: 'google-cloud-automl',
62+
});
63+
const latestRelease = undefined;
64+
const release = await strategy.buildReleasePullRequest(
65+
commits,
66+
latestRelease
67+
);
68+
expect(release!.version?.toString()).to.eql(expectedVersion);
69+
});
70+
it('builds a release pull request', async () => {
71+
const expectedVersion = '0.123.5';
72+
const strategy = new Salesforce({
73+
targetBranch: 'main',
74+
github,
75+
component: 'some-salesforce-package',
76+
packageName: 'some-salesforce-package',
77+
});
78+
const latestRelease = {
79+
tag: new TagName(Version.parse('0.123.4'), 'some-salesforce-package'),
80+
sha: 'abc123',
81+
notes: 'some notes',
82+
};
83+
const pullRequest = await strategy.buildReleasePullRequest(
84+
commits,
85+
latestRelease
86+
);
87+
expect(pullRequest!.version?.toString()).to.eql(expectedVersion);
88+
});
89+
it('detects a default component', async () => {
90+
const expectedVersion = '0.123.5';
91+
const strategy = new Salesforce({
92+
targetBranch: 'main',
93+
github,
94+
});
95+
const commits = [
96+
...buildMockConventionalCommit(
97+
'fix(deps): update dependency com.google.cloud:google-cloud-storage to v1.120.0'
98+
),
99+
];
100+
const latestRelease = {
101+
tag: new TagName(Version.parse('0.123.4'), 'salesforce-test-repo'),
102+
sha: 'abc123',
103+
notes: 'some notes',
104+
};
105+
const getFileContentsStub = sandbox.stub(
106+
github,
107+
'getFileContentsOnBranch'
108+
);
109+
getFileContentsStub
110+
.withArgs('sfdx-project.json', 'main')
111+
.resolves(buildGitHubFileContent(fixturesPath, 'sfdx-project.json'));
112+
const pullRequest = await strategy.buildReleasePullRequest(
113+
commits,
114+
latestRelease
115+
);
116+
expect(pullRequest!.version?.toString()).to.eql(expectedVersion);
117+
});
118+
it('detects a default packageName', async () => {
119+
const expectedVersion = '0.123.5';
120+
const strategy = new Salesforce({
121+
targetBranch: 'main',
122+
github,
123+
component: 'abc-123',
124+
});
125+
const commits = [
126+
...buildMockConventionalCommit(
127+
'fix(deps): update dependency com.google.cloud:google-cloud-storage to v1.120.0'
128+
),
129+
];
130+
const latestRelease = {
131+
tag: new TagName(Version.parse('0.123.4'), 'salesforce-test-repo'),
132+
sha: 'abc123',
133+
notes: 'some notes',
134+
};
135+
const getFileContentsStub = sandbox.stub(
136+
github,
137+
'getFileContentsOnBranch'
138+
);
139+
getFileContentsStub
140+
.withArgs('sfdx-project.json', 'main')
141+
.resolves(buildGitHubFileContent(fixturesPath, 'sfdx-project.json'));
142+
const pullRequest = await strategy.buildReleasePullRequest(
143+
commits,
144+
latestRelease
145+
);
146+
expect(pullRequest!.version?.toString()).to.eql(expectedVersion);
147+
});
148+
it('handles missing sfdx-project.json', async () => {
149+
sandbox
150+
.stub(github, 'getFileContentsOnBranch')
151+
.rejects(new FileNotFoundError('stub/path'));
152+
const strategy = new Salesforce({
153+
targetBranch: 'main',
154+
github,
155+
});
156+
const latestRelease = {
157+
tag: new TagName(Version.parse('0.123.4'), 'some-salesforce-package'),
158+
sha: 'abc123',
159+
notes: 'some notes',
160+
};
161+
assert.rejects(async () => {
162+
await strategy.buildReleasePullRequest(commits, latestRelease);
163+
}, MissingRequiredFileError);
164+
});
165+
});
166+
describe('buildUpdates', () => {
167+
it('builds common files', async () => {
168+
const strategy = new Salesforce({
169+
targetBranch: 'main',
170+
github,
171+
component: 'google-cloud-automl',
172+
packageName: 'google-cloud-automl-pkg',
173+
});
174+
sandbox.stub(github, 'findFilesByFilenameAndRef').resolves([]);
175+
const latestRelease = undefined;
176+
const release = await strategy.buildReleasePullRequest(
177+
commits,
178+
latestRelease
179+
);
180+
const updates = release!.updates;
181+
assertHasUpdate(updates, 'CHANGELOG.md', Changelog);
182+
assertHasUpdate(updates, 'sfdx-project.json', SfdxProjectJson);
183+
});
184+
});
185+
});

0 commit comments

Comments
 (0)