@google-cloud/release-brancher
Version:
Cut release branches
322 lines • 12.1 kB
JavaScript
;
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.Runner = void 0;
const rest_1 = require("@octokit/rest");
const code_suggester_1 = require("code-suggester");
const yaml = __importStar(require("js-yaml"));
const fetch = require('node-fetch');
class Runner {
constructor(options) {
this.branchName = options.branchName;
this.targetTag = options.targetTag;
this.releaseType = options.releaseType;
this.octokit = new rest_1.Octokit({ auth: options.gitHubToken, request: { fetch } });
this.upstreamRepo = options.upstreamRepo;
this.upstreamOwner = options.upstreamOwner;
this.pullRequestTitle = options.pullRequestTitle;
}
async getTargetSha(tag) {
var _a;
const resp = await this.octokit.git.listMatchingRefs({
owner: this.upstreamOwner,
repo: this.upstreamRepo,
ref: `tags/${this.targetTag}`,
});
return (_a = resp.data.find(ref => {
return ref.ref === `refs/tags/${tag}`;
})) === null || _a === void 0 ? void 0 : _a.object.sha;
}
async getBranch(branchName) {
try {
const existing = await this.octokit.git.getRef({
owner: this.upstreamOwner,
repo: this.upstreamRepo,
ref: `heads/${branchName}`,
});
return existing.data.ref;
}
catch (e) {
const err = e;
if (err.status === 404) {
return undefined;
}
throw e;
}
}
async getDefaultBranch() {
const response = await this.octokit.repos.get({
owner: this.upstreamOwner,
repo: this.upstreamRepo,
});
return response.data.default_branch;
}
/**
* Creates a branch from the tag specified at initialization.
* If the branch already exists, this is a no-op.
*
* @throws {Error} If the specified tag cannot be found.
* @returns {string} The new branch ref.
*/
async createBranch() {
const existing = await this.getBranch(this.branchName);
if (existing) {
console.log(`branch ${this.branchName} already exists`);
return existing;
}
const sha = await this.getTargetSha(this.targetTag);
if (!sha) {
console.log(`couldn't find SHA for tag ${this.targetTag}`);
throw new Error(`couldn't find SHA for tag ${this.targetTag}`);
}
console.log(`creating branch ${this.branchName} as SHA ${sha}`);
const response = await this.octokit.git.createRef({
owner: this.upstreamOwner,
repo: this.upstreamRepo,
ref: `refs/heads/${this.branchName}`,
sha,
});
return response.data.ref;
}
async getFileContents(path) {
try {
const response = (await this.octokit.repos.getContent({
owner: this.upstreamOwner,
repo: this.upstreamRepo,
path,
}));
return Buffer.from(response.data.content, 'base64').toString('utf8');
}
catch (e) {
const err = e;
if (err.status === 404) {
return undefined;
}
throw e;
}
}
updateReleasePleaseConfig(content) {
const config = yaml.load(content);
const branches = config.branches || [];
delete config.branches;
if (branches.find(branch => {
return branch.branch === this.branchName;
})) {
// already found branch
return undefined;
}
const newConfig = yaml.load(content);
const newBranchConfig = {
...config,
branch: this.branchName,
};
if (this.releaseType) {
newBranchConfig.releaseType = this.releaseType;
}
branches.push(newBranchConfig);
newConfig.branches = branches;
return yaml.dump(newConfig, {
noRefs: true,
});
}
updateSyncRepoSettings(content) {
const config = yaml.load(content);
const branches = config.branchProtectionRules || [];
if (branches.length === 0) {
// no configured branch protection - we cannot infer what to do
throw new Error('Cannot find existing branch protection rules: aborting');
}
if (branches.find(branch => {
return branch.pattern === this.branchName;
})) {
// already found branch
return undefined;
}
// TODO: consider fetching the default branch name from the GitHub API
const found = branches[0];
const newRule = {
...found,
pattern: this.branchName,
};
branches.push(newRule);
config.branchProtectionRules = branches;
return yaml.dump(config, {
noRefs: true,
});
}
/**
* Opens a pull request against the default branch with updated
* release-please and sync-repo-settings configurations. If an existing
* pull request already exists, it will force-push changes to the
* existing pull request.
*
* @returns {number} The pull request number.
*/
async createPullRequest() {
const changes = new Map();
let content = await this.getFileContents('.github/release-please.yml');
if (content) {
const newContent = this.updateReleasePleaseConfig(content);
if (newContent) {
changes.set('.github/release-please.yml', {
mode: '100644',
content: newContent,
});
}
}
content = await this.getFileContents('.github/sync-repo-settings.yaml');
if (content) {
const newContent = this.updateSyncRepoSettings(content);
if (newContent) {
changes.set('.github/sync-repo-settings.yaml', {
mode: '100644',
content: newContent,
});
}
}
const defaultBranch = await this.getDefaultBranch();
const message = this.pullRequestTitle === undefined
? `build: configure branch ${this.branchName} as a release branch`
: this.pullRequestTitle;
return await (0, code_suggester_1.createPullRequest)(this.octokit, changes, {
upstreamRepo: this.upstreamRepo,
upstreamOwner: this.upstreamOwner,
message,
title: message,
description: 'enable releases',
primary: defaultBranch,
branch: `release-brancher/${this.branchName}`,
force: true,
fork: false,
});
}
/**
* Replace the default branch name in GitHub actions config.
*/
updateWorkflow(content, defaultBranch) {
var _a, _b;
const config = yaml.load(content);
let updated = false;
if ((_a = config.on.push) === null || _a === void 0 ? void 0 : _a.branches) {
const index = config.on.push.branches.indexOf(defaultBranch);
if (index !== -1) {
config.on.push.branches[index] = this.branchName;
updated = true;
}
}
if ((_b = config.on.pull_request) === null || _b === void 0 ? void 0 : _b.branches) {
const index = config.on.pull_request.branches.indexOf(defaultBranch);
if (index !== -1) {
config.on.pull_request.branches[index] = this.branchName;
updated = true;
}
}
if (updated) {
return yaml.dump(config, {
noRefs: true,
});
}
return content;
}
/**
* Opens a pull request against the new release branch with updated
* GitHub action workflows. If an existing pull request already exists,
* it will force-push changes to the existing pull request.
*
* @returns {number} The pull request number.
*/
async createWorkflowPullRequest() {
const sha = await this.getTargetSha(this.targetTag);
if (!sha) {
console.log(`couldn't find SHA for tag ${this.targetTag}`);
throw new Error(`couldn't find SHA for tag ${this.targetTag}`);
}
const response = await this.octokit.git.getTree({
owner: this.upstreamOwner,
repo: this.upstreamRepo,
tree_sha: sha,
recursive: 'true',
});
const changes = new Map();
const files = response.data.tree.filter(file => {
return (file.path &&
file.path.startsWith('.github/workflows/') &&
file.path.endsWith('.yaml'));
});
const defaultBranch = await this.getDefaultBranch();
for (const file of files) {
const content = await this.getFileContents(file.path);
if (content) {
const newContent = this.updateWorkflow(content, defaultBranch);
if (newContent !== content) {
changes.set(file.path, {
mode: '100644',
content: newContent,
});
}
}
}
// For java-lts type, we want a release through a releasable commit ('feat: ') immediately
// after creating the protected branch.
const message = (this.releaseType === 'java-lts' ? 'feat' : 'ci') +
': configure the protected branch';
return await (0, code_suggester_1.createPullRequest)(this.octokit, changes, {
upstreamRepo: this.upstreamRepo,
upstreamOwner: this.upstreamOwner,
message,
title: message,
description: 'Configures CI for branch',
branch: `release-brancher/ci/${this.branchName}`,
primary: this.branchName,
force: true,
fork: false,
});
}
}
exports.Runner = Runner;
//# sourceMappingURL=release-brancher.js.map