Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prelease Version Strategies #147

Open
jbolda opened this issue Feb 17, 2021 · 3 comments
Open

Prelease Version Strategies #147

jbolda opened this issue Feb 17, 2021 · 3 comments
Labels
question Further information is requested

Comments

@jbolda
Copy link
Owner

jbolda commented Feb 17, 2021

These are raw notes from a few collaboration meetings I have had on the subject.

Prelease

  1. Incremental prerelease
  2. Major version bump

We want a way to configure it that every time a commit is created, we publish on that tag.

prereleases

main branch is 2.1.0
=> open feat A branch with a minor bump => 2.2.0-feat-a.0 (dirty publish with the tag name, hit registry to get existing tags)
=> open feat B branch with a minor bump => 2.2.0-feat-b.0 (dirty publish with the tag name, hit registry to get existing tags)

main branch could get a prelease on next or similar off of HEAD

major version bump

  • Config value flip
  • config value to prevent major version

process into using this

  1. Previews by default without any change files on main branch.
  2. Next turn it on for PRs? First step onramp could be opt into prelease on each PR commit (opt into prelease?). (Every package gets a bump if no changes exist yet.)
  3. Next step would be with PRs and change files.

semver: https://semver.org/

  1. A pre-release version MAY be denoted by appending a hyphen and a series of dot separated identifiers immediately following the patch version. Identifiers MUST comprise only ASCII alphanumerics and hyphens [0-9A-Za-z-]. Identifiers MUST NOT be empty. Numeric identifiers MUST NOT include leading zeroes. Pre-release versions have a lower precedence than the associated normal version. A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility requirements as denoted by its associated normal version. Examples: 1.0.0-alpha, 1.0.0-alpha.1, 1.0.0-0.3.7, 1.0.0-x.7.z.92, 1.0.0-x-y-z.–.
  1. Build metadata MAY be denoted by appending a plus sign and a series of dot separated identifiers immediately following the patch or pre-release version. Identifiers MUST comprise only ASCII alphanumerics and hyphens [0-9A-Za-z-]. Identifiers MUST NOT be empty. Build metadata MUST be ignored when determining version precedence. Thus two versions that differ only in the build metadata, have the same precedence. Examples: 1.0.0-alpha+001, 1.0.0+20130313144700, 1.0.0-beta+exp.sha.5114f85, 1.0.0+21AF26D3—-117B344092BD.

Language Version Numbering

dotnet

Parsing: https://www.npmjs.com/package/xml-js

dart/flutter

Parsing: https://www.npmjs.com/package/js-yaml

@jbolda
Copy link
Owner Author

jbolda commented Mar 19, 2021

Looking at specific version numbers for prereleases, we could use git rev-list --count to retrieve the number of commits on a branch and use that as the incremental number when publishing. This would let us avoid requiring writing the version number out to the filesystem somewhere (which would otherwise be needed to determine the version number next time we publish, ie the default publishing situation). With prerelease numbers, the ^ requirement ends up being alpha sorted, so rc > beta > alpha. We likely want to be careful with breaking these and it would make some sense to prepend an extra "identifier" for PR related preview packages. With the sorting mechanism, capitalization sorts first. We can add -PR- to the previews so that -PR- < alpha.

Snippets below from the semver package.

const semver = require(`semver`);

const testabunch = (pre) =>
  [
    `2.0.0-alpha.1`,
    `2.0.0-beta.1`,
    `2.0.0-${pre}feat/preview-release.1`,
    `2.0.0-${pre}feat/preview-release.2`,
    `2.0.0-${pre}feat/preview-release.3`,
    `2.0.0-${pre}feat/preview_release.3`,
    `2.0.0-${pre}feat/xreview-release.1`,
    `2.0.0-${pre}feat/preview-release.9`,
    `2.0.0-${pre}feat/seview>release.1`,
  ].map((v) => v.replace(`/`, `-`).replace(`_`, `-`).replace(`>`, `-`));

const range = `^2.0.0-alpha.1`;

const list1 = testabunch("PR-").map((test) => {
  console.log(test, semver.satisfies(test, range));
  return test;
});

console.log(`%%%%%%%%%%`);
console.log(semver.maxSatisfying(list1, range));
console.log(semver.minSatisfying(list1, range));

console.log(`*****************************`);

const list2 = testabunch("").map((test) => {
  console.log(test, semver.satisfies(test, range));
  return test;
});

console.log(`%%%%%%%%%%`);
console.log(semver.maxSatisfying(list2, range));


console.log(`%%%%%%%%%%`);

console.log(`%%%%%block%%%%%`);
console.log(semver.inc("0.4.0-boop.10", "prepatch", "boop"));
console.log(semver.inc("0.4.0-boop.10", "preminor", "boop"));
console.log(semver.inc("0.4.0-boop.10", "premajor", "boop"));
console.log(semver.inc("0.4.0-boop.10", "prerelease", "boop"));
console.log(`%%%%%block%%%%%`);
console.log(semver.inc("0.4.0", "prepatch", "boop"));
console.log(semver.inc("0.4.0", "preminor", "boop"));
console.log(semver.inc("0.4.0", "premajor", "boop"));
console.log(semver.inc("0.4.0", "prerelease", "boop"));

console.log(`%%%%%block%%%%%`);
console.log(semver.inc("0.4.0-boop.10", "patch"));
console.log(semver.inc("0.4.0-boop.10", "minor"));
console.log(semver.inc("0.4.0-boop.10", "major"));
console.log(`%%%%%block%%%%%`);
console.log(semver.inc("0.4.0", "patch"));
console.log(semver.inc("0.4.0", "minor"));
console.log(semver.inc("0.4.0", "major"));

returns:

❯ node test.js
2.0.0-alpha.1 true
2.0.0-beta.1 true
2.0.0-PR-feat-preview-release.1 false
2.0.0-PR-feat-preview-release.2 false
2.0.0-PR-feat-preview-release.3 false
2.0.0-PR-feat-preview-release.3 false
2.0.0-PR-feat-xreview-release.1 false
2.0.0-PR-feat-preview-release.9 false
2.0.0-PR-feat-seview-release.1 false
%%%%%%%%%%
2.0.0-beta.1
2.0.0-alpha.1
*****************************
2.0.0-alpha.1 true
2.0.0-beta.1 true
2.0.0-feat-preview-release.1 true
2.0.0-feat-preview-release.2 true
2.0.0-feat-preview-release.3 true
2.0.0-feat-preview-release.3 true
2.0.0-feat-xreview-release.1 true
2.0.0-feat-preview-release.9 true
2.0.0-feat-seview-release.1 true
%%%%%%%%%%
2.0.0-feat-xreview-release.1
2.0.0-alpha.1
%%%%%%%%%%
%%%%%block%%%%%
0.4.1-boop.0
0.5.0-boop.0
1.0.0-boop.0
0.4.0-boop.11
%%%%%block%%%%%
0.4.1-boop.0
0.5.0-boop.0
1.0.0-boop.0
0.4.1-boop.0
%%%%%block%%%%%
0.4.0
0.4.0
1.0.0
%%%%%block%%%%%
0.4.1
0.5.0
1.0.0

@jbolda
Copy link
Owner Author

jbolda commented Mar 31, 2021

Preleases and Preview Packages

Terminology

A typical release includes running the version and publish commands. This will increment version numbers, update the changelogs, and then publish the packages to the configuration registry (or whatever configured command).

A prerelease is the same release process except using a version number that includes a prerelease identifier such as 0.1.0-rc.3 where rc.50 is the identifier. A prerelease expects to be committed with an incremented version number. This is most likely the process that you will use for an alpha, beta, or rc type release.

A preview package has some similarities except we do not expect a commit. The version command is run followed immediately by a publish. This also means that subsequent publishes are not aware of previous publishes. Considering this, the default dot separate identifier is a unix-epoch timestamp. This release lends itself best to -next.X release tags or previews of a PR or feature branch.

Using Preleases

This is a mode that you toggled on. It creates a pre.json with configuration and will house the "state" of the process. It has a tag property that determines the prerelease identifier. A prerelease accumulates change files throughout the process. The accumulated changes help the next run of version determine which changes have already been applied. If all previous changes were patch and the next change is a minor, we could increment both the minor and .... When the prerelease mode is "exited", we apply every change file to the changelog and apply a version bump with whatever is the "highest" bump.

{
  "tag": "next",
  "changes": [
    "this-is-a-change-file",
    "another-change-file"
  ]
}

Note: For those familiar with changeset, this mode draws heavy inspiration from their work.

Implementation Notes (only put this as comments within the code)

Using the semver package, the prerelease is the only thing that bumps the dot identifier. So if there is a prerelease identifier, we can assume we need to do a prerelease bump. We do need to determine if the existing major.minor.patch has been already correctly bumped. If we had only patch previously and the next is a minor, we actually need to bump with a preminor. We can compare the accumulated and applied changes vs the new changes to see if the new changes require a higher bump.

So the sequencing is to accumulate all of the changes and compare applied vs new. If the applied < new changes, then bump with pre+new change. If applied >= new, then we can just bump with a prerelease. We only need to apply bumps to packages with new changes.

Using Previews

Raw Chat Notes

Dates could work: Date.now(). It was something that was considered when I wrote the action but I forgot why we went with SHA. But again, having SHA in the version is useful for checking if your latest push has been published or not. Ideally, we'd be able to use the npm update or yarn upgrade-interactive commands and have things just work.

@simulacrum/[email protected]
That could work. I'm torn because this is a) monotonic b) gives an indication of freshness, but c) while less cumbersome than a sha, is still not as clean as a simple integer

I do wish we could get a clean integer, but the npm view package@tag version strategy is reliant on npm tags which feel fragile (and all the tags bother me a bit but that seems moreso based in opinion and most people don't care). I also don't think it will work in any other ecosystem. I don't know any other package managers that implement tags, so it is probable this will only ever be an npm solution, and potentially impossible to use when thinking about publishing services / yml + no package manager or registry. So regardless if we would implement the npm view, we would need a fallback. Presuming we can implement this with the same APIs as prereleases, I think it is likely that we could decide to add functionality to use npm view if you so choose as an opt-in type strategy, but the initial implementation seems to point towards a timestamp being the best path.

next is the first use cases that matters to us because several kinds of projects already use it. Preview packages are nice to have and are better understood in Node.js projects but not known or expected by others

I suspect the happy path for implementation will be some sort of templating of the version number. It may open up the option where we could allow some configuration of the template that would enable a consumer to do something special for npm if they choose to. Especially if we have some kind of configuration package mechanism like babel. This would allow us to define a convertor configuration for each ecosystem and then use that configuration on all sites. If we need to change it, it'd change in one place or override in a specific repo.

@minkimcello
Copy link
Collaborator

minkimcello commented Apr 2, 2021

So it seems we have three different scenarios where we need to generate preview packages:

  1. For pull requests (our current previews workflow on frontside projects)
  2. For beta branches (basically a hybrid of preview and release but making sure it doesn't use the latest tag)
  3. For Version PRs (this would run by default as opposed to the other two where users must opt in)

I was thinking we could call case 1 dev preview, case 2, beta preview, and case 3 rc preview. Prerelease seems it could apply to both cases 2 and 3. 🤷‍♂️

Anyway for our first step we're going to only focus on dev and not think too much about beta or rc (although halfway through typing this out I have a hunch the other two scenarios will probably just require a tiny bit of tweaking from the first case to get it working but we'll see).

The main mechanics of being able to build and publish preview packages with covector should be rather simple because:

  1. We just need to branch out an if statement to modify the covector version command to bump the versions differently and to ensure it doesn't delete the change files.
  2. Modify the action (or workflow) so that we can execute both version and publish in one workflow run.

Here is a diagram of a rough overview of how the preview publishing can be implemented:
Screen Shot 2021-04-02 at 1 50 53 PM

However, there are a few things to consider to polish the UX of generating preview packages:

Opting In

We want the developers to opt-in so that preview packages are not published for every pull request.

  • This can be achieved in a few different ways. I originally typed out the diagram to require users to run a command from their local machines. But I'm thinking we can make this even better by using pr labels (as Charles originally suggested). We won't trigger the workflow on labels but for every push to a PR, we can query all the labels to determine if we should run the preview processes.
    • This means if a developer forgets to add a label or decides later into the PR to generate preview packages, they will need to create the label and push another commit to trigger the preview publishing.
    • The use of labels might be a good idea because it'll help with tidying things up when users merge their PRs. One aspect of this preview package process that's different from before is the whole opting-in system. If we modify or toggle any booleans within a file in the repository, that needs to eventually be undone. By using labels, the label will become irrelevant to the main branch once the PR gets merged or closed.
      • For this reason I don't think we should add the array of change files in pre.json even if we do go with the approach of modifying local files because for every new change file, they will need to run the preview command.

Consuming Previews

There seems to be a lot of caveats around prereleases and semver. But there are two things we want to be able to achieve:

  1. Check if the package is ready to be used
    • The concern I have with not using SHA is that there's really no easy way to confirm if a package has been successfully published. Normally doing npm view package@tag will display the last published and the developer could cross check their latest commit (commit ABCDEF) with the published version (x.x.x-ABCDEF) to confirm if it's ready to be downloaded.
  2. Update the preview package dependency easily without having to delete lock files

We discovered that as long as there is no stable version, the prerelease increments will be calculated:

  • If 1.1.1 exists, installing ^1.1.1-branch.1 will install 1.1.1
  • If 1.1.1 doesn't exist, installing ^1.1.1-branch.1 will install 1.1.1-branch.{highest and not necessarily the latest}

To address the needs of checking if a preview package has been published successfully, we can modify the preview version by slicing the commit to just three or four characters: 1.1.1-branch.1ABC.

The second requirement is a little bit trickier. I'm taking a wild guess but I think there might be some under-the-hood caching going on. Say there are .1 and .2 published. Installing ^.1 should get .2 and for the most part it does. But then afterwards if I publish .3 and .4, installing ^.1 will still bring me .2. But if I bump the dependency to ^.3 I'll get .4 and then only afterwards if I go back to ^.1, it will fetch .4.

Screen Shot 2021-04-02 at 3 30 00 PM

The second issue to the second requirement is that as soon as 1.1.1 is published, having a dependency version of 1.1.1-preview.* will resolve to 1.1.1 so we will eventually come across a situation where a developer will have no choice but to rebase or else they won't be able to publish preview packages anymore. We could always have the action notify the developer in their pull request with a comment or with an error whenever the bumped version of their pre-release has been published in a separate PR but this doesn't seem very UX-friendly.

I think the easiest and most robust/temporary solution would be to generate a Github PR comment to instruct people which version to install?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants