diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4733329..c46a843 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,37 +3,48 @@ name: Create Release on: workflow_dispatch: inputs: + previous_release: + description: 'Existing tag to release from' + required: true + type: string bump: - description: Version bump + description: 'Version bump -- required when Previous Release is a stable tag; leave "none" when Previous Release is an RC (to continue its RC series or promote it to stable)' required: true type: choice options: + - none - patch - minor - major - -permissions: - contents: write - -concurrency: - group: release - cancel-in-progress: false + create_rc: + description: 'Create RC tag (Note: unchecked = stable release / promote an existing RC to stable)' + required: false + type: boolean + default: false jobs: release: + if: github.ref_type == 'branch' && (github.ref_name == 'main' || endsWith(github.ref_name, '.rc') || endsWith(github.ref_name, '.x-maintenance')) runs-on: ubuntu-latest + concurrency: + group: release + cancel-in-progress: false + permissions: + contents: write + issues: write + pull-requests: write steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 + ssh-key: ${{ secrets.RDKCM_DEPLOY_KEY }} - name: Validate release branch shell: bash run: | branch="${GITHUB_REF_NAME}" - case "$branch" in main|*.rc|*.*.x-maintenance) echo "Release allowed from branch: $branch" @@ -47,73 +58,96 @@ jobs: - name: Compute next version id: version env: + PREV: ${{ inputs.previous_release }} BUMP: ${{ inputs.bump }} + CREATE_RC: ${{ inputs.create_rc }} shell: bash run: | git fetch --tags --force origin - # Only consider strict vMAJOR.MINOR.PATCH tags reachable from HEAD; - # pre-release/other v-prefixed tags (e.g. v0.6.4.rc1) would break the - # numeric parsing below, and tags from unrelated history shouldn't - # count as "latest" for this release. - semver_tags() { - git tag --merged HEAD --list 'v*' --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' - } - - # Reuse an existing HEAD tag if this is a rerun after a partial failure. - existing="$(git tag --points-at HEAD --list 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n1)" - if [ -n "$existing" ]; then - echo "HEAD already tagged as ${existing}; resuming partial release." - prev="$(semver_tags | awk -v ex="${existing}" '$0 != ex { print; exit }')" - [ -z "$prev" ] && prev="v0.0.0" - echo "latest=$prev" >> "$GITHUB_OUTPUT" - echo "next=$existing" >> "$GITHUB_OUTPUT" - exit 0 + if ! git tag --list "$PREV" | grep -qxF "$PREV"; then + echo "ERROR: '${PREV}' is not an existing tag on this repo. Check Previous Release for typos." + exit 1 fi - latest="$(semver_tags | head -n1)" - - if [ -z "$latest" ]; then - latest="v0.0.0" - fi + # Bump a vX.Y.Z string by the requested level + bump_stable() { + local ver="${1#v}" major minor patch + IFS='.' read -r major minor patch <<< "$ver" + case "$2" in + major) echo "v$((major+1)).0.0" ;; + minor) echo "v${major}.$((minor+1)).0" ;; + patch) echo "v${major}.${minor}.$((patch+1))" ;; + esac + } - ver="${latest#v}" - IFS='.' read -r major minor patch <<< "$ver" + is_rc=false + is_promotion=false + next="" - case "$BUMP" in - major) - major=$((major + 1)) - minor=0 - patch=0 - ;; - minor) - minor=$((minor + 1)) - patch=0 - ;; - patch) - patch=$((patch + 1)) - ;; - esac + if [[ "$PREV" =~ ^v[0-9]+\.[0-9]+\.[0-9]+\.rc[0-9]+$ ]]; then + # Previous release is itself an RC -- no bump makes sense here; + # either continue its RC series or promote it straight to stable. + if [ "$BUMP" != "none" ]; then + echo "ERROR: A version bump isn't supported when Previous Release is an RC tag (${PREV}). Select its underlying stable tag instead." + exit 1 + fi + base_stable="${PREV%%.rc*}" + if [ "$CREATE_RC" = "true" ]; then + rc_num="${PREV##*.rc}" + next="${base_stable}.rc$((rc_num + 1))" + is_rc=true + else + next="$base_stable" + is_promotion=true + fi + + elif [[ "$PREV" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + # Previous release is a stable tag -- a bump is required to know + # what comes next. + if [ "$BUMP" = "none" ]; then + echo "ERROR: Select a version bump to release from stable tag ${PREV}." + exit 1 + fi + next_stable="$(bump_stable "$PREV" "$BUMP")" + if [ "$CREATE_RC" = "true" ]; then + next="${next_stable}.rc1" + is_rc=true + else + next="$next_stable" + fi - next="v${major}.${minor}.${patch}" + else + echo "ERROR: '${PREV}' doesn't look like a release tag (expected vX.Y.Z or vX.Y.Z.rcN)." + exit 1 + fi - echo "latest=$latest" - echo "next=$next" - echo "latest=$latest" >> "$GITHUB_OUTPUT" - echo "next=$next" >> "$GITHUB_OUTPUT" + echo "baseline=$PREV" | tee -a "$GITHUB_OUTPUT" + echo "next=$next" | tee -a "$GITHUB_OUTPUT" + echo "is_rc=$is_rc" | tee -a "$GITHUB_OUTPUT" + echo "is_promotion=$is_promotion" | tee -a "$GITHUB_OUTPUT" - name: Validate release has changes env: - LATEST: ${{ steps.version.outputs.latest }} + BASELINE: ${{ steps.version.outputs.baseline }} + IS_PROMOTION: ${{ steps.version.outputs.is_promotion }} shell: bash run: | - if [ "$LATEST" = "v0.0.0" ]; then - echo "No previous tag found; proceeding with initial release." + if [ "$IS_PROMOTION" = "true" ]; then + if ! git merge-base --is-ancestor "${BASELINE}" HEAD; then + echo "ERROR: ${BASELINE} is not an ancestor of HEAD; select a descendant commit from this branch before promoting to stable." + exit 1 + fi + echo "Promoting ${BASELINE} to stable release; current HEAD is a descendant." exit 0 fi - - if git rev-list --count "${LATEST}..HEAD" | grep -q '^0$'; then - echo "No new commits since ${LATEST}; refusing to create a duplicate release." + if ! git merge-base --is-ancestor "${BASELINE}" HEAD; then + echo "ERROR: ${BASELINE} is not an ancestor of HEAD; select a Previous Release tag from this branch." + exit 1 + fi + count="$(git rev-list --count "${BASELINE}..HEAD")" + if [ "$count" -eq 0 ]; then + echo "ERROR: No new commits since ${BASELINE}; refusing to create a duplicate release." exit 1 fi @@ -123,11 +157,12 @@ jobs: shell: bash run: | if grep -q '^## \[Unreleased\]' CHANGELOG.md; then - echo "ERROR: CHANGELOG.md still has an [Unreleased] section. Update it before releasing." + echo "ERROR: CHANGELOG.md has an [Unreleased] section. Update it before releasing." exit 1 fi version="${NEXT#v}" - top="$(awk 'match($0, /^## \[([^]]+)\]/, m) { print m[1]; exit }' CHANGELOG.md)" + version="${version%%.rc*}" + top="$(awk '/^## \[[^]]+\]/ { sub(/^## \[/, ""); sub(/\].*$/, ""); print; exit }' CHANGELOG.md)" if [ -z "$top" ] || [ "$top" != "$version" ]; then echo "ERROR: Top CHANGELOG.md entry (${top:-}) does not match version ${version}. Move the ${version} entry to the top." exit 1 @@ -136,16 +171,24 @@ jobs: - name: Create tag env: TAG: ${{ steps.version.outputs.next }} + GH_TOKEN: ${{ secrets.SEMANTIC_RELEASE_TOKEN }} shell: bash run: | if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then - echo "Tag ${TAG} already exists on origin; continuing." + git fetch --force origin "refs/tags/${TAG}:refs/tags/${TAG}" + if [ "$(git rev-parse "refs/tags/${TAG}^{commit}")" != "$(git rev-parse HEAD)" ]; then + echo "ERROR: Tag ${TAG} already exists on origin but does not point to the current HEAD; refusing to overwrite its release assets." + exit 1 + fi + echo "Tag ${TAG} already exists at the current HEAD; resuming the release." exit 0 fi - - git config user.name "github-actions" - git config user.email "github-actions@github.com" - + # Identity is just for the annotated tag's metadata -- SEMANTIC_RELEASE_TOKEN is + # fine for this lookup, it doesn't touch ref creation. + author_login="$(gh api user --jq '.login')" + author_name="$(gh api user --jq '.name // .login')" + git config user.name "$author_name" + git config user.email "${author_login}@users.noreply.github.com" git tag -a "${TAG}" -m "Release ${TAG}" git push origin "${TAG}" @@ -155,36 +198,31 @@ jobs: VERSION: ${{ steps.version.outputs.next }} shell: bash run: | - # Reuse the existing script: strips .github/, writes .version, produces build/firebolt-cpp-client-.tar.gz + bare="${VERSION#v}" ./.github/scripts/mk-release-package.sh \ - --version "${VERSION}" \ + --version "${bare}" \ --package firebolt-cpp-client - - bare="${VERSION#v}" - archive_name="firebolt-cpp-client-${bare}.tar.gz" - checksum_name="${archive_name}.sha256" - - sha256sum "build/${archive_name}" > "build/${checksum_name}" - - # Extract the top CHANGELOG.md section as release notes (same as the old generateNotesCmd). awk '/^## / { if (s) exit; s=1 } s{ print }' CHANGELOG.md > release_notes.md - - echo "archive=build/${archive_name}" >> "$GITHUB_OUTPUT" - echo "checksum=build/${checksum_name}" >> "$GITHUB_OUTPUT" + echo "archive=build/firebolt-cpp-client-${bare}.tar.gz" >> "$GITHUB_OUTPUT" - name: Create GitHub release env: - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ secrets.SEMANTIC_RELEASE_TOKEN }} TAG: ${{ steps.version.outputs.next }} + IS_RC: ${{ steps.version.outputs.is_rc }} ARCHIVE: ${{ steps.asset.outputs.archive }} - CHECKSUM: ${{ steps.asset.outputs.checksum }} shell: bash run: | + release_flags=() + [ "$IS_RC" = "true" ] && release_flags+=(--prerelease) if gh release view "$TAG" >/dev/null 2>&1; then - gh release upload "$TAG" "$ARCHIVE" "$CHECKSUM" --clobber + gh release upload "$TAG" "$ARCHIVE" --clobber else + # The tag already exists (pushed in "Create tag"), so this only + # creates the release object against it. gh release create "$TAG" \ --title "$TAG" \ --notes-file release_notes.md \ - "$ARCHIVE" "$CHECKSUM" + "${release_flags[@]}" \ + "$ARCHIVE" fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 01f9653..2db38e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ - `Localization.timeZone` getter and `onTimeZoneChanged` event - `Device.dolbyAtmosExperienceAvailable` getter and `onDolbyAtmosExperienceAvailableChanged` event - `Device.osName`, `Device.osVersion`, and `Device.firmware` - - `SpeechSynthesis` (`speak`, `cancel`, `pause`, `resume`, `voices`, `subscribeVoiceChanged`, `onUtteranceEvent`) + - `SpeechSynthesis` (`speak`, `cancel`, `pause`, `resume`, `voices`, `subscribeOnVoicesChanged`, `onUtteranceEvent`) - `VideoOutput` implementation ### Changed