Skip to content
Merged
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
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ To setup and run the automated tests for the plugin SDK samples, check the [test

## Releasing a New Version

Releasing the SDK is a single command, run from the project root by a maintainer with publish rights on npm. It writes the new version, publishes the package, points the 23 sample projects at it, and then commits, tags and pushes the release.
Releasing the SDK is a single command, run from the project root by a maintainer with publish rights on npm. It writes the new version, publishes the package, points the 23 sample projects at it, and then commits, tags and pushes the release to the main repository.

Called without an argument it releases the version that follows the current one: a stable version moves to the next patch, while a pre-release moves its own counter and stays on its channel.

Expand All @@ -249,22 +249,27 @@ Called with a version it releases exactly that version, which is how a new pre-r

The npm dist-tag follows from the version itself: a stable version is published as `latest`, and a pre-release under its own channel (`beta`, `rc`, and so on), so installing the package without asking for a tag keeps returning the stable release.

Every invocation also takes `--dry-run`, which prints each step of the release, from `npm version` to the final `git push`, and ends with `[dry-run] nothing was published, committed or pushed`.
Every invocation also takes `--dry-run`, which prints each step of the release, from `npm version` to the final atomic push to the main repository, and ends with `[dry-run] nothing was published, committed or pushed`.

```bash
./scripts/publish-version.sh 1.0.0 --dry-run
```

Five guards stop a release before it changes anything:
Six guards stop a release before it changes anything:

- a branch that is not a release branch of the main repository, in sync with it: `Error: branch main tracks origin/main, which is not a release branch of bigbluebutton/bigbluebutton-html-plugin-sdk.` The main repository and its release branches are declared in `scripts/lib/release-branches.json`, so a new release line is a one-line addition there.
- a version that is not a version: `"1.0" is not a semantic version. Expected MAJOR.MINOR.PATCH, optionally followed by a pre-release such as -beta.1.`
- a version that does not move the package forward: `Error: 0.1.26 is not higher than the current version 0.1.26.`
- a git tag that is already taken: `Error: tag v0.1.27 already exists.`
- a working tree with uncommitted changes: `Error: the working tree has uncommitted changes.`
- a main repository remote that is not reachable over ssh: `Error: remote upstream is the main repository bigbluebutton/bigbluebutton-html-plugin-sdk, but its URL is not an ssh URL (...).` Releases are pushed over ssh, and the message prints the exact `git remote set-url` that points the remote at its ssh URL.

The branch guard reads the remote tip over the network to confirm the branch is in sync, and under `--dry-run` it only reports what a real run would refuse, so a dry run still works from any branch or clone. The "not higher" and "uncommitted changes" guards belong to the npm stage: the git-only re-run described below skips them by design.

The release is pushed to the remote that is the main repository, found by matching each remote's `owner/name` (so a fork checked out as `origin` never receives it). When more than one remote matches, the first over ssh in `git remote` order wins, and the chosen remote is named in the output. The branch commit and the tag then go up in a single atomic push, so a release can never land half on one remote and half on another.

Releases are pushed over ssh on purpose. An https remote with a working credential helper would push just as well, but a missing credential there fails halfway through, after the package is already on npm. Requiring ssh turns that into an upfront refusal with a one-line fix, before anything is published.

The two stages of the release can be toggled off independently through environment variables, and both honor `--dry-run`:

```bash
Expand All @@ -279,7 +284,7 @@ PUBLISH_TO_GITHUB=false ./scripts/publish-version.sh

publishes to npm without recording the release in git.

The version arithmetic lives in `scripts/lib/version.js`, and the branch guard in `scripts/lib/check-release-branch.sh`.
The version arithmetic lives in `scripts/lib/version.js`, and the branch check and remote-URL helpers in `scripts/lib/check-git-preconditions.sh`.

## API

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,51 @@
#!/bin/bash

# This script refuses to release from the wrong place: the current branch has to track a
# release branch of the main repository, and be in sync with it. Which repository is the
# main one, and which of its branches are releasable, is declared in release-branches.json
# next to this script.
# Git preconditions of a release: the check below refuses to release from the wrong branch or
# clone, and the URL helpers are shared with the remote resolution in publish-version.sh.
#
# The comparisons are made on the remote side: the remote is identified by the owner/name
# its URL ends in, which matches https, ssh and local paths all the same way, and the
# branch by the name it has on that remote, which is not always the local name. Checking
# the sync reads the remote tip over the network.
#
# Usage: ./scripts/lib/check-release-branch.sh [--dry-run]
# Usage: ./scripts/lib/check-git-preconditions.sh [--dry-run]

# The owner/name a remote URL points at: drop a trailing .git, keep the last two segments.
# Normalizes https, ssh and local-path URLs the same way.
repository_of_remote_url() {
printf '%s' "$1" | sed 's/\.git$//' | awk -F'[/:]' '{print $(NF-1) "/" $NF}'
}

# Whether a URL is ssh: the ssh:// scheme, or git's scp-like host:path form (colon before any slash).
is_ssh_remote_url() {
case "$1" in
ssh://*) return 0 ;;
*://*) return 1 ;;
*:*)
case "${1%%:*}" in
*/*) return 1 ;;
?*) return 0 ;;
*) return 1 ;;
esac
;;
*) return 1 ;;
esac
}

# Remotes whose URL is the given repository, in git remote order; unreadable URLs are skipped.
main_repository_remotes() {
main_repository="$1"

for remote in $(git remote); do
if ! url=$(git remote get-url "$remote" 2> /dev/null); then
continue
fi

if [ "$(repository_of_remote_url "$url")" = "$main_repository" ]; then
printf '%s\n' "$remote"
fi
done

return 0
}

# publish-version.sh sources this file for the helpers above; the check below runs on execution.
[ "${BASH_SOURCE[0]}" = "$0" ] || return 0

set -e

Expand Down Expand Up @@ -62,7 +97,7 @@ if ! REMOTE_URL=$(git remote get-url "$REMOTE" 2> /dev/null); then
"Releases leave from a release branch ($RELEASE_BRANCHES) of $MAIN_REPOSITORY."
fi

REMOTE_REPOSITORY=$(printf '%s' "$REMOTE_URL" | sed 's/\.git$//' | awk -F'[/:]' '{print $(NF-1) "/" $NF}')
REMOTE_REPOSITORY=$(repository_of_remote_url "$REMOTE_URL")

if [ "$REMOTE_REPOSITORY" != "$MAIN_REPOSITORY" ]; then
refuse "branch $BRANCH tracks $UPSTREAM_REF on $REMOTE_URL, which is not the main repository $MAIN_REPOSITORY" \
Expand Down
89 changes: 82 additions & 7 deletions scripts/publish-version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ set -e
THIS_SCRIPT_PATH=$(dirname "$(readlink -f "$0")")
PROJECT_DIR=$(realpath "$THIS_SCRIPT_PATH/..")

# Sourced for the remote-URL helpers; executed further down as the branch check.
# shellcheck source=lib/check-git-preconditions.sh
. "$THIS_SCRIPT_PATH/lib/check-git-preconditions.sh"

# The toggles are environment overrides, not in-file switches: flipping a stage must not require
# editing the file, because an edited script dirties the tree and the clean-tree check below
# would refuse the release.
Expand Down Expand Up @@ -62,9 +66,82 @@ done

cd "$PROJECT_DIR"

# Refusal of the remote resolution: a real run stops with an error before anything is
# published; a dry run only reports it and ends the way every dry run does.
release_remote_refusal() {
if [ "$DRY_RUN_FLAG" = "--dry-run" ]; then
echo "[dry-run] $1; a real run would stop here."
echo "$2"
echo "$3"
echo "[dry-run] nothing was published, committed or pushed"
exit 0
fi

echo "Error: $1."
echo "$2"
echo "$3"
exit 1
}

# Picks the push target: the first remote that is the main repository over ssh, in git
# remote order. Sets RELEASE_REMOTE and RELEASE_BRANCH, which the push site reads.
resolve_release_remote() {
# Cleared first, so values inherited from the environment cannot pose as a resolution.
RELEASE_REMOTE=""
RELEASE_BRANCH=""

local main_repository upstream_ref main_remotes remote first_main_remote first_main_url

main_repository=$(node -pe "require('$THIS_SCRIPT_PATH/lib/release-branches.json').mainRepository")

# A missing upstream refuses on a real run; a dry run goes on and still resolves the
# remote, leaving the branch empty for the push line to show as a placeholder.
if upstream_ref=$(git rev-parse --abbrev-ref "@{upstream}" 2> /dev/null); then
RELEASE_BRANCH="${upstream_ref#*/}"
elif [ "$DRY_RUN_FLAG" != "--dry-run" ]; then
echo "Error: could not resolve the branch this release is pushed to."
exit 1
fi

main_remotes=$(main_repository_remotes "$main_repository")
first_main_remote=""

for remote in $main_remotes; do
[ -n "$first_main_remote" ] || first_main_remote="$remote"
if is_ssh_remote_url "$(git remote get-url "$remote" 2> /dev/null)"; then
RELEASE_REMOTE="$remote"
break
fi
done

if [ -n "$RELEASE_REMOTE" ]; then
echo "Releasing to remote $RELEASE_REMOTE ($main_repository)"
return 0
fi

if [ -n "$first_main_remote" ]; then
first_main_url=$(git remote get-url "$first_main_remote" 2> /dev/null || true)
release_remote_refusal \
"remote $first_main_remote is the main repository $main_repository, but its URL is not an ssh URL ($first_main_url)" \
"Releases are pushed over ssh, so point that remote at the ssh URL:" \
" git remote set-url $first_main_remote git@github.com:$main_repository.git"
else
release_remote_refusal \
"no remote is the main repository $main_repository" \
"Add it as a remote over ssh:" \
" git remote add upstream git@github.com:$main_repository.git"
fi
}

# The branch is checked before anything else, so a release from the wrong branch or
# clone stops while nothing has been changed yet.
"$THIS_SCRIPT_PATH/lib/check-release-branch.sh" $DRY_RUN_FLAG
"$THIS_SCRIPT_PATH/lib/check-git-preconditions.sh" $DRY_RUN_FLAG

# Resolved by name, so a fork checked out as origin never receives the release. The npm-only
# stage does not push and stays usable from an https clone.
if [ "$PUBLISH_TO_GITHUB" = "true" ]; then
resolve_release_remote
fi

DEPENDENCY_NAME=$(node -pe "require('./package.json').name")
CURRENT_VERSION=$(node -pe "require('./package.json').version")
Expand Down Expand Up @@ -149,8 +226,7 @@ if [ "$DRY_RUN_FLAG" = "--dry-run" ]; then
echo "[dry-run] git add ${#FILES_TO_COMMIT[@]} version files (package.json/package-lock.json of the project and of the samples)"
echo "[dry-run] git commit -m \"Bump version to $NEW_VERSION\""
echo "[dry-run] git tag v$NEW_VERSION"
echo "[dry-run] git push origin v$NEW_VERSION"
echo "[dry-run] git push"
echo "[dry-run] git push --atomic $RELEASE_REMOTE HEAD:refs/heads/${RELEASE_BRANCH:-<release-branch>} refs/tags/v$NEW_VERSION"
# --- end point the samples at the new version, commit, tag and push to github ---
fi

Expand Down Expand Up @@ -212,10 +288,9 @@ if [ "$PUBLISH_TO_GITHUB" = "true" ]; then

git tag "v$NEW_VERSION"

git push origin "v$NEW_VERSION"

git push
# One atomic push: the branch commit and the tag land together, or neither does.
git push --atomic "$RELEASE_REMOTE" "HEAD:refs/heads/$RELEASE_BRANCH" "refs/tags/v$NEW_VERSION"

echo "Committed, tagged and pushed v$NEW_VERSION"
echo "Committed, tagged and pushed v$NEW_VERSION to $RELEASE_REMOTE"
# --- end point the samples at the new version, commit, tag and push to github ---
fi
Loading