Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
49 changes: 35 additions & 14 deletions __tests__/create-branch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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 () => {
Expand Down
6 changes: 3 additions & 3 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
51 changes: 42 additions & 9 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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/<branch>)
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)
Expand Down
40 changes: 32 additions & 8 deletions src/create-branch.ts
Original file line number Diff line number Diff line change
@@ -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/<branch>)
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');
Expand Down