diff --git a/README.md b/README.md index 0cffbf6..c7f36f4 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,24 @@ # Create Branch GitHub Action -This action creates a new branch with the same commit reference as the branch it is being ran on, or your chosen reference when specified. +This action creates a new branch with the same commit reference as the branch it is being run on, or your chosen reference when specified. ## Inputs ### `branch` -**Optional** The name of the branch to create. Default `"release-candidate"`. If your branch conains forward slashes (`/`) use the full branch reference. Instead of `/long/branch/name` use `refs/heads/long/branch/name`. It's an issue with the GitHub API https://gist.github.com/jasonrudolph/10727108 +**Optional** The name of the branch to create. Default `"release-candidate"`. If your branch contains forward slashes (`/`) use the full branch reference. Instead of `/long/branch/name` use `refs/heads/long/branch/name`. It's an issue with the GitHub API https://gist.github.com/jasonrudolph/10727108 + +If the branch already exists, the action will attempt to update the branch reference to point at the provided `sha` (or the current event `sha`) via a fast-forward update. The action does not force-push or rewrite history; if the update is not a fast-forward the API will reject the update. ### `sha` -**Optional** The SHA1 value for the branch reference. +**Optional** The SHA-1 value for the branch reference. ## Outputs ### `created` -Boolean value representing whether or not a new branch was created. +Boolean value representing whether the action successfully created or updated the branch reference. `true` means the branch reference was created or updated to point at the requested SHA; `false` indicates the operation failed. ## Example usage diff --git a/__tests__/create-branch.test.ts b/__tests__/create-branch.test.ts index eb5b252..e8e47fb 100644 --- a/__tests__/create-branch.test.ts +++ b/__tests__/create-branch.test.ts @@ -12,7 +12,9 @@ describe('Create a branch based on the input', () => { let octokitMock = { rest: { git: { - createRef: jest.fn() + createRef: jest.fn(), + updateRef: jest.fn(), + getRef: jest.fn() }, repos: { getBranch: jest.fn() @@ -27,41 +29,60 @@ describe('Create a branch based on the input', () => { }); it('gets a branch', async () => { - octokitMock.rest.repos.getBranch.mockRejectedValue(new HttpError()) + octokitMock.rest.git.getRef.mockRejectedValue(new HttpError()) + octokitMock.rest.git.createRef.mockResolvedValue({ data: { ref: `refs/heads/${branch}` } }); process.env.GITHUB_REPOSITORY = 'peterjgrainger/test-action-changelog-reminder' await createBranch(githubMock, context, branch) - expect(octokitMock.rest.repos.getBranch).toHaveBeenCalledWith({ + expect(octokitMock.rest.git.getRef).toHaveBeenCalledWith({ + ref: `heads/${branch}`, repo: 'test-action-changelog-reminder', - owner: 'peterjgrainger', - branch + owner: 'peterjgrainger' }) }); it('Creates a new branch if not already there', async () => { - octokitMock.rest.repos.getBranch.mockRejectedValue(new HttpError()) + octokitMock.rest.git.getRef.mockRejectedValue(new HttpError()) + octokitMock.rest.git.createRef.mockResolvedValue({ data: { ref: 'refs/heads/release-v1' } }); await createBranch(githubMock, contextMock, branch) - expect(octokitMock.rest.git.createRef).toHaveBeenCalledWith({ + expect(octokitMock.rest.git.createRef).toHaveBeenCalledWith(expect.objectContaining({ ref: 'refs/heads/release-v1', sha: 'ebb4992dc72451c1c6c99e1cce9d741ec0b5b7d7' - }) + })) }); it('Creates a new branch from a given commit SHA', async () => { - octokitMock.rest.repos.getBranch.mockRejectedValue(new HttpError()) + octokitMock.rest.git.getRef.mockRejectedValue(new HttpError()) + octokitMock.rest.git.createRef.mockResolvedValue({ data: { ref: 'refs/heads/release-v1' } }); await createBranch(githubMock, contextMock, branch, sha) - expect(octokitMock.rest.git.createRef).toHaveBeenCalledWith({ + expect(octokitMock.rest.git.createRef).toHaveBeenCalledWith(expect.objectContaining({ ref: 'refs/heads/release-v1', sha: 'ffac537e6cbbf934b08745a378932722df287a53' - }) + })) }) it('Replaces refs/heads in branch name', async () => { - octokitMock.rest.repos.getBranch.mockRejectedValue(new HttpError()) + octokitMock.rest.git.getRef.mockRejectedValue(new HttpError()) + octokitMock.rest.git.createRef.mockResolvedValue({ data: { ref: 'refs/heads/release-v1' } }); await createBranch(githubMock, contextMock, `refs/heads/${branch}`) - expect(octokitMock.rest.git.createRef).toHaveBeenCalledWith({ + expect(octokitMock.rest.git.createRef).toHaveBeenCalledWith(expect.objectContaining({ ref: 'refs/heads/release-v1', sha: 'ebb4992dc72451c1c6c99e1cce9d741ec0b5b7d7' - }) + })) + }); + + it('Updates existing branch via fast-forward', async () => { + // getRef succeeds (ref exists) + octokitMock.rest.git.getRef.mockResolvedValue({ data: { ref: 'refs/heads/release-v1', object: { sha: contextMock.sha } } }); + octokitMock.rest.git.updateRef.mockResolvedValue({ data: { ref: 'refs/heads/release-v1' } }); + + const result = await createBranch(githubMock, contextMock, branch, undefined); + + expect(octokitMock.rest.git.updateRef).toHaveBeenCalledWith(expect.objectContaining({ + ref: 'heads/release-v1', + sha: contextMock.sha + })); + + expect(result).toBe(true); }); it('Fails if github token isn\'t defined', async () => { diff --git a/action.yml b/action.yml index cf378a6..73114ff 100644 --- a/action.yml +++ b/action.yml @@ -6,13 +6,13 @@ branding: color: 'green' inputs: branch: - description: 'The branch to create' + description: 'The branch to create (or update if it already exists via a fast-forward)' default: 'release-candidate' sha: - description: 'The SHA1 value for the branch reference' + description: 'The SHA1 value for the branch reference. If omitted the action uses the event SHA.' outputs: created: - description: 'Boolean value representing whether or not a new branch was created.' + description: 'Boolean value representing whether the branch reference was successfully created or updated to the requested SHA.' runs: using: 'node20' main: 'dist/index.js' diff --git a/dist/index.js b/dist/index.js index aff86c1..b966c2a 100644 --- a/dist/index.js +++ b/dist/index.js @@ -10117,10 +10117,29 @@ function wrappy (fn, cb) { /***/ }), /***/ 6719: -/***/ (function(__unused_webpack_module, exports) { +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { "use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (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; +}; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { @@ -10132,29 +10151,43 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.createBranch = void 0; +const core = __importStar(__nccwpck_require__(2186)); function createBranch(getOctokit, context, branch, sha) { - var _a; return __awaiter(this, void 0, void 0, function* () { const toolkit = getOctokit(githubToken()); // Sometimes branch might come in with refs/heads already branch = branch.replace('refs/heads/', ''); const ref = `refs/heads/${branch}`; - // throws HttpError if branch already exists. + const refPath = `heads/${branch}`; + const targetSha = sha || context.sha; + core.debug(`Target ref: ${ref} (createRef), refPath: ${refPath} (getRef/updateRef), target SHA: ${targetSha}`); + // Check if branch already exists using git refs API (heads/) try { - yield toolkit.rest.repos.getBranch(Object.assign(Object.assign({}, context.repo), { branch })); + const refData = yield toolkit.rest.git.getRef(Object.assign({ ref: refPath }, context.repo)); + core.debug(`Found ref via getRef: ${JSON.stringify(refData.data)}`); + // If ref exists, update it to target SHA + const resp = yield toolkit.rest.git.updateRef(Object.assign({ ref: refPath, sha: targetSha }, context.repo)); + core.debug(`updateRef response: ${JSON.stringify(resp.data)}`); + return isValidRefResponse(resp, ref); } catch (error) { + // If the ref was not found, create it. Other errors bubble up. if (error.name === 'HttpError' && error.status === 404) { - const resp = yield toolkit.rest.git.createRef(Object.assign({ ref, sha: sha || context.sha }, context.repo)); - return ((_a = resp === null || resp === void 0 ? void 0 : resp.data) === null || _a === void 0 ? void 0 : _a.ref) === ref; - } - else { - throw Error(error); + core.debug(`Ref not found via getRef, creating new branch`); + const resp = yield toolkit.rest.git.createRef(Object.assign({ ref, sha: targetSha }, context.repo)); + core.debug(`createRef response: ${JSON.stringify(resp.data)}`); + return isValidRefResponse(resp, ref); } + core.debug(`Unexpected error while checking/creating ref: ${error.name} ${error.status} ${error.message}`); + throw Error(error); } }); } exports.createBranch = createBranch; +function isValidRefResponse(resp, expectedRef) { + var _a; + return ((_a = resp === null || resp === void 0 ? void 0 : resp.data) === null || _a === void 0 ? void 0 : _a.ref) === expectedRef; +} function githubToken() { const token = process.env.GITHUB_TOKEN; if (!token) diff --git a/src/create-branch.ts b/src/create-branch.ts index 118c0fd..4c906b3 100644 --- a/src/create-branch.ts +++ b/src/create-branch.ts @@ -1,32 +1,56 @@ import { Context } from '@actions/github/lib/context'; +import * as core from '@actions/core'; export async function createBranch(getOctokit: any, context: Context, branch: string, sha?: string) { const toolkit = getOctokit(githubToken()); // Sometimes branch might come in with refs/heads already branch = branch.replace('refs/heads/', ''); const ref = `refs/heads/${branch}`; - - // throws HttpError if branch already exists. + const refPath = `heads/${branch}`; + const targetSha = sha || context.sha; + + core.debug(`Target ref: ${ref} (createRef), refPath: ${refPath} (getRef/updateRef), target SHA: ${targetSha}`); + // Check if branch already exists using git refs API (heads/) try { - await toolkit.rest.repos.getBranch({ + const refData = await toolkit.rest.git.getRef({ + ref: refPath, + ...context.repo, + }); + + core.debug(`Found ref via getRef: ${JSON.stringify(refData.data)}`); + + // If ref exists, update it to target SHA + const resp = await toolkit.rest.git.updateRef({ + ref: refPath, + sha: targetSha, ...context.repo, - branch, }); + + core.debug(`updateRef response: ${JSON.stringify(resp.data)}`); + return isValidRefResponse(resp, ref); } catch (error: any) { + // If the ref was not found, create it. Other errors bubble up. if (error.name === 'HttpError' && error.status === 404) { + core.debug(`Ref not found via getRef, creating new branch`); const resp = await toolkit.rest.git.createRef({ ref, - sha: sha || context.sha, + sha: targetSha, ...context.repo, }); - return resp?.data?.ref === ref; - } else { - throw Error(error); + core.debug(`createRef response: ${JSON.stringify(resp.data)}`); + return isValidRefResponse(resp, ref); } + + core.debug(`Unexpected error while checking/creating ref: ${error.name} ${error.status} ${error.message}`); + throw error; } } +function isValidRefResponse(resp: any, expectedRef: string): boolean { + return resp?.data?.ref === expectedRef; +} + function githubToken(): string { const token = process.env.GITHUB_TOKEN; if (!token) throw ReferenceError('No token defined in the environment variables');