UNPKG

@google-cloud/release-brancher

Version:
311 lines 11.8 kB
"use strict"; // 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 (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __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")); 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 }); 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