Skip to content

Commit

Permalink
Add GH release workflow and bump-version action, fix hard-coded c…
Browse files Browse the repository at this point in the history
…lient version (#66)

## Problem
We have a private function which returns a hard-coded version number
which is used when we build the user-agent for requests originating from
the client. This is difficult to maintain while releasing because Go
releases are handled through GitHub and Git tags, and requires us to
keep things in sync manually.

I also missed updating this for `v1.0.0`, so it needs to be bumped
anyways.

We're also missing any kind of "official" release process for the Go
client, which would be beneficial in standardizing versioning and the
process for releasing the client now that we're >v1.0.0. I'd rather we
have a GitHub workflow similar to the other clients even if it primarily
involves bumping a hard-coded version file, and pushing a tag.

## Solution
- Replace the old hard-coded version with a new `internal/version.go`
file which packages and holds `internal.Version` to be used within the
client. This is the file that CI will manage and commit updates towards
as we bump versions.
- Add new GitHub action `.github/actions/bump-version` allowing us to
easily bump the `currentVersion` using a specific `releaseLevel` (major,
minor, patch), `isPrerelease`, and `prereleaseSuffix` if desired. This
action was something @jhamon originally added to the
[pinecone-python-client](https://github.com/pinecone-io/pinecone-python-client/tree/main/.github/actions/bump-version).
I've lifted it here with minor alterations to support just returning a
new `version_tag` rather than modifying a file using `fs` which it does
in the Python repo.
- I would like to work on getting this action shared properly through
GitHub so that we can reuse with one source of truth, but that will take
some additional work beyond the scope of this fix. I've added a ticket:
https://app.asana.com/0/1203260648987893/1208039955828667/f
- Add new GitHub workflow `./github/workflows/release.yaml` to
facilitate releasing the Go SDK by updating `internal/version.go`, and
pushing the new tag. For prerelease, we just push the tag without
committing.

## Type of Change
- [X] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] This change requires a documentation update
- [X] Infrastructure change (CI configs, etc)
- [ ] Non-code change (docs, etc)
- [ ] None of the above: (explain here)

## Test 
I'll need to get the CI files into source before I can test the actual
release process. My thinking here is I've updated the hard-coded value
to the current `v1.0.0`, and I'd like to release a `v1.0.1` to fix the
current problem with the user-agent not matching. I think this is
reasonable, but let me know if you don't agree.

The `bump-version` action itself was lifted directly from @jhamon's work
including unit tests, so I think that is mostly safe. I'd spend the bulk
of the review looking at my approach in `release.yaml`.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1206945560195919
  • Loading branch information
austin-denoble authored Aug 16, 2024
1 parent aee839b commit b1754c6
Show file tree
Hide file tree
Showing 12 changed files with 4,171 additions and 2 deletions.
1 change: 1 addition & 0 deletions .github/actions/bump-version/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
36 changes: 36 additions & 0 deletions .github/actions/bump-version/action.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const core = require("./core");

function bumpVersion(currentVersion, bumpType, prerelease) {
let newVersion = calculateNewVersion(currentVersion, bumpType);

if (prerelease) {
newVersion = `${newVersion}.${prerelease}`;
}
core.setOutput("previous_version", currentVersion);
core.setOutput("previous_version_tag", `v${currentVersion}`);
core.setOutput("version", newVersion);
core.setOutput("version_tag", `v${newVersion}`);
}

function calculateNewVersion(currentVersion, bumpType) {
const [major, minor, patch] = currentVersion.split(".");
let newVersion;

switch (bumpType) {
case "major":
newVersion = `${parseInt(major) + 1}.0.0`;
break;
case "minor":
newVersion = `${major}.${parseInt(minor) + 1}.0`;
break;
case "patch":
newVersion = `${major}.${minor}.${parseInt(patch) + 1}`;
break;
default:
throw new Error(`Invalid bumpType: ${bumpType}`);
}

return newVersion;
}

module.exports = { bumpVersion };
114 changes: 114 additions & 0 deletions .github/actions/bump-version/action.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
const action = require("./action");
const core = require("./core");

jest.mock("./core");

describe("bump-version", () => {
test("bump major", () => {
action.bumpVersion("1.2.3", "major", "");

expect(core.setOutput).toHaveBeenCalledWith("previous_version", "1.2.3");
expect(core.setOutput).toHaveBeenCalledWith(
"previous_version_tag",
"v1.2.3"
);
expect(core.setOutput).toHaveBeenCalledWith("version", "2.0.0");
expect(core.setOutput).toHaveBeenCalledWith("version_tag", "v2.0.0");
});

test("bump minor: existing minor and patch", () => {
action.bumpVersion("1.2.3", "minor", "");

expect(core.setOutput).toHaveBeenCalledWith("previous_version", "1.2.3");
expect(core.setOutput).toHaveBeenCalledWith(
"previous_version_tag",
"v1.2.3"
);
expect(core.setOutput).toHaveBeenCalledWith("version", "1.3.0");
expect(core.setOutput).toHaveBeenCalledWith("version_tag", "v1.3.0");
});

test("bump minor: with no patch", () => {
action.bumpVersion("1.2.0", "minor", "");

expect(core.setOutput).toHaveBeenCalledWith("previous_version", "1.2.0");
expect(core.setOutput).toHaveBeenCalledWith(
"previous_version_tag",
"v1.2.0"
);
expect(core.setOutput).toHaveBeenCalledWith("version", "1.3.0");
expect(core.setOutput).toHaveBeenCalledWith("version_tag", "v1.3.0");
});

test("bump minor: from existing patch", () => {
action.bumpVersion("2.2.3", "minor", "");

expect(core.setOutput).toHaveBeenCalledWith("previous_version", "2.2.3");
expect(core.setOutput).toHaveBeenCalledWith(
"previous_version_tag",
"v2.2.3"
);
expect(core.setOutput).toHaveBeenCalledWith("version", "2.3.0");
expect(core.setOutput).toHaveBeenCalledWith("version_tag", "v2.3.0");
});

test("bump patch: existing patch", () => {
action.bumpVersion("1.2.3", "patch", "");

expect(core.setOutput).toHaveBeenCalledWith("previous_version", "1.2.3");
expect(core.setOutput).toHaveBeenCalledWith(
"previous_version_tag",
"v1.2.3"
);
expect(core.setOutput).toHaveBeenCalledWith("version", "1.2.4");
expect(core.setOutput).toHaveBeenCalledWith("version_tag", "v1.2.4");
});

test("bump patch: minor with no patch", () => {
action.bumpVersion("1.2.0", "patch", "");

expect(core.setOutput).toHaveBeenCalledWith("previous_version", "1.2.0");
expect(core.setOutput).toHaveBeenCalledWith(
"previous_version_tag",
"v1.2.0"
);
expect(core.setOutput).toHaveBeenCalledWith("version", "1.2.1");
expect(core.setOutput).toHaveBeenCalledWith("version_tag", "v1.2.1");
});

test("bump patch: major with no minor or patch", () => {
action.bumpVersion("1.0.0", "patch", "");

expect(core.setOutput).toHaveBeenCalledWith("previous_version", "1.0.0");
expect(core.setOutput).toHaveBeenCalledWith(
"previous_version_tag",
"v1.0.0"
);
expect(core.setOutput).toHaveBeenCalledWith("version", "1.0.1");
expect(core.setOutput).toHaveBeenCalledWith("version_tag", "v1.0.1");
});

test("bump patch: major with minor", () => {
action.bumpVersion("1.1.0", "patch", "");

expect(core.setOutput).toHaveBeenCalledWith("previous_version", "1.1.0");
expect(core.setOutput).toHaveBeenCalledWith(
"previous_version_tag",
"v1.1.0"
);
expect(core.setOutput).toHaveBeenCalledWith("version", "1.1.1");
expect(core.setOutput).toHaveBeenCalledWith("version_tag", "v1.1.1");
});

test("prerelease suffix provided", () => {
action.bumpVersion("1.2.3", "patch", "rc1");

expect(core.setOutput).toHaveBeenCalledWith("previous_version", "1.2.3");
expect(core.setOutput).toHaveBeenCalledWith(
"previous_version_tag",
"v1.2.3"
);
expect(core.setOutput).toHaveBeenCalledWith("version", "1.2.4.rc1");
expect(core.setOutput).toHaveBeenCalledWith("version_tag", "v1.2.4.rc1");
});
});
29 changes: 29 additions & 0 deletions .github/actions/bump-version/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: 'pinecone-io/bump-version'

description: 'Bumps a given semantic version number based on a bumpType and prereleaseSuffix'

inputs:
currentVersion:
description: 'The current version of the client to bump from'
required: true
bumpType:
description: 'The type of version bump (major, minor, patch)'
required: true
prereleaseSuffix:
description: 'Optional prerelease identifier to append to the version number'
required: false
default: ''

outputs:
version:
description: 'The new version number'
version_tag:
description: 'The new version tag'
previous_version:
description: 'The previous version number'
previous_version_tag:
description: 'The previous version tag'

runs:
using: 'node20'
main: 'index.js'
79 changes: 79 additions & 0 deletions .github/actions/bump-version/core.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copied these commands out of the github actions toolkit
// because actually depending on @actions/core requires me to check
// in node_modules and 34MB of dependencies, which I don't want to do.

const fs = require("fs");
const os = require("os");

function getInput(name, options) {
const val =
process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] || "";
if (options && options.required && !val) {
throw new Error(`Input required and not supplied: ${name}`);
}

if (options && options.trimWhitespace === false) {
return val;
}

return val.trim();
}

function toCommandValue(input) {
if (input === null || input === undefined) {
return "";
} else if (typeof input === "string" || input instanceof String) {
return input;
}
return JSON.stringify(input);
}

function prepareKeyValueMessage(key, value) {
const delimiter = `delimiter_${Math.floor(Math.random() * 100000)}`;
const convertedValue = toCommandValue(value);

// These should realistically never happen, but just in case someone finds a
// way to exploit uuid generation let's not allow keys or values that contain
// the delimiter.
if (key.includes(delimiter)) {
throw new Error(
`Unexpected input: name should not contain the delimiter "${delimiter}"`
);
}

if (convertedValue.includes(delimiter)) {
throw new Error(
`Unexpected input: value should not contain the delimiter "${delimiter}"`
);
}

return `${key}<<${delimiter}${os.EOL}${convertedValue}${os.EOL}${delimiter}`;
}

function setOutput(name, value) {
const filePath = process.env["GITHUB_OUTPUT"] || "";
if (filePath) {
return issueFileCommand("OUTPUT", prepareKeyValueMessage(name, value));
}

process.stdout.write(os.EOL);
issueCommand("set-output", { name }, toCommandValue(value));
}

function issueFileCommand(command, message) {
const filePath = process.env[`GITHUB_${command}`];
if (!filePath) {
throw new Error(
`Unable to find environment variable for file command ${command}`
);
}
if (!fs.existsSync(filePath)) {
throw new Error(`Missing file at path: ${filePath}`);
}

fs.appendFileSync(filePath, `${toCommandValue(message)}${os.EOL}`, {
encoding: "utf8",
});
}

module.exports = { getInput, setOutput };
8 changes: 8 additions & 0 deletions .github/actions/bump-version/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const action = require("./action");
const core = require("./core");

action.bumpVersion(
core.getInput("currentVersion"),
core.getInput("bumpType"),
core.getInput("prereleaseSuffix")
);
3 changes: 3 additions & 0 deletions .github/actions/bump-version/jest.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"verbose": true
}
Loading

0 comments on commit b1754c6

Please sign in to comment.