diff --git a/.github/workflows/component-release-finish-on-approval.yml b/.github/workflows/component-release-finish-on-approval.yml new file mode 100755 index 00000000..27720f6f --- /dev/null +++ b/.github/workflows/component-release-finish-on-approval.yml @@ -0,0 +1,120 @@ +name: Component Release Finish On Approval + +on: + pull_request_review: + types: [submitted] + +permissions: + contents: write + pull-requests: write + +concurrency: + group: release-finish-${{ github.event.pull_request.head.ref }} + cancel-in-progress: false + +jobs: + finish-release: + name: Finish release when PR is approved + if: > + github.event.review.state == 'approved' && + github.event.pull_request.base.ref == 'develop' && + startsWith(github.event.pull_request.head.ref, 'release/') + runs-on: ubuntu-latest + env: + REPO: ${{ github.repository }} + ACTOR: ${{ github.event.review.user.login }} + GH_TOKEN: ${{ secrets.RDKCM_RDKE }} + RELEASE_BRANCH: ${{ github.event.pull_request.head.ref }} + + steps: + - name: Check approval status + id: approvals + run: | + set -euo pipefail + pr_number="${{ github.event.pull_request.number }}" + decision=$(gh pr view --repo "${REPO}" "${pr_number}" --json reviewDecision -q '.reviewDecision') + echo "PR #${pr_number} review decision: ${decision}" + if [ "${decision}" = "APPROVED" ]; then + echo "should_finish=true" >> "$GITHUB_OUTPUT" + else + echo "PR is not fully approved yet. Waiting." + echo "should_finish=false" >> "$GITHUB_OUTPUT" + fi + + - name: Checkout repository + if: steps.approvals.outputs.should_finish == 'true' + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ssh-key: ${{ secrets.RDKCM_DEPLOY_KEY }} + + - name: Install release tools + if: steps.approvals.outputs.should_finish == 'true' + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y git-flow + + - name: Configure git identity + if: steps.approvals.outputs.should_finish == 'true' + run: | + set -euo pipefail + git config user.name "${ACTOR}" + git config user.email "${ACTOR}@users.noreply.github.com" + + - name: Finish release and push + if: steps.approvals.outputs.should_finish == 'true' + run: | + set -euo pipefail + release_version="${RELEASE_BRANCH#release/}" + + git fetch --prune origin + + # Skip if tag already exists + if git ls-remote --exit-code --tags origin "refs/tags/${release_version}" >/dev/null 2>&1; then + echo "Tag ${release_version} already exists on origin. Skipping release finish." + exit 0 + fi + + # Checkout the release branch + if git show-ref --verify --quiet "refs/heads/${RELEASE_BRANCH}"; then + git checkout "${RELEASE_BRANCH}" + elif git ls-remote --exit-code --heads origin "${RELEASE_BRANCH}" >/dev/null 2>&1; then + git checkout -b "${RELEASE_BRANCH}" "origin/${RELEASE_BRANCH}" + else + echo "${RELEASE_BRANCH} does not exist locally or on origin." + exit 1 + fi + + # Ensure main and develop exist locally + git checkout main 2>/dev/null || git checkout -b main origin/main + git checkout develop 2>/dev/null || git checkout -b develop origin/develop + git checkout "${RELEASE_BRANCH}" + + # Initialize git-flow + git flow init -d + + # Finish release: merges to main + develop, creates tag + git flow release finish -m "Release ${release_version}" "${release_version}" + git push origin main + git push origin --tags + git push origin develop + git push origin --delete "${RELEASE_BRANCH}" || true + + - name: Close the release PR + if: steps.approvals.outputs.should_finish == 'true' + run: | + set -euo pipefail + pr_number="${{ github.event.pull_request.number }}" + gh pr close "${pr_number}" --repo "${REPO}" --comment "Release finished by automation. Merged via git flow release finish." || true + + - name: Workflow summary + if: always() + run: | + { + echo "## Release Finish On Approval" + echo "- Repository: ${REPO}" + echo "- Release branch: ${RELEASE_BRANCH}" + echo "- Triggered by review: ${{ github.event.review.state }}" + echo "- Finished release: ${{ steps.approvals.outputs.should_finish || 'false' }}" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/component-release-workflow-usage.md b/.github/workflows/component-release-workflow-usage.md new file mode 100755 index 00000000..2c62d0e3 --- /dev/null +++ b/.github/workflows/component-release-workflow-usage.md @@ -0,0 +1,97 @@ +# Automated Release Workflow + +Automates main release and hotfix flows via GitHub Actions. + +## Workflows + +### 1. Component Release (`component-release.yml`) + +Triggered manually from the **Actions** tab via `workflow_dispatch`. + +#### Inputs + +| Input | Required | Description | +|-------|----------|-------------| +| `release_version` | Yes | Release version (for example: `5.2.0`, `5.2.0-rc1`) | +| `release_type` | Yes | `main` or `hotfix` | +| `release_mode` | Yes | `approvable` or `auto-complete` (ignored for hotfix) | +| `source_branch` | Hotfix only | Support branch for hotfix (for example: `support/5.0`) | + +#### Concurrency + +Runs are serialized per release key: + +- `component-release--` + +This prevents two runs for the same version/type from racing on branches/tags. + +#### Release Modes + +**Main release - auto-complete** +1. Validates version format. +2. Runs `git flow release start` -> changelog -> `release publish` -> `release finish`. +3. Pushes `main`, `develop`, and tags. + +**Main release - approvable** +1. Validates version format. +2. Runs `git flow release start` -> changelog -> `release publish`. +3. Creates a PR: `release/` -> `develop`. +4. Stops and waits for PR approval. +5. On approval, the second workflow finishes the release. + +**Hotfix (always auto-complete)** +1. Requires `source_branch` input. +2. Validates `source_branch` against `^[A-Za-z0-9._/-]+$`. +3. Creates/uses `hotfix/` from `source_branch`, updates changelog, merges back into `source_branch` only. +4. Creates tag ``. +5. Pushes only `source_branch` and tag (`main`/`develop` are not pushed). + +### 2. Component Release Finish On Approval (`component-release-finish-on-approval.yml`) + +Triggered on approved review for `release/*` PRs targeting `develop`. + +#### What it does +1. Verifies PR review decision is `APPROVED`. +2. Checks out release branch. +3. Runs `git flow release finish` (merge to `main` + `develop`, create tag). +4. Pushes `main`, `develop`, and tags. +5. Closes the release PR. + +## Authentication and Secrets + +Both workflows use: + +- `RDKCM_DEPLOY_KEY`: SSH private key used by checkout and git push operations. +- `RDKCM_RDKE`: token used by `gh` API/CLI calls. + +## Failure Cleanup Safety + +On failure, cleanup only removes refs created by the current run: + +- Deletes tag only if the run created that tag. +- Deletes `release/` only if the run created that local release branch. +- Deletes `hotfix/` only if the run created that local hotfix branch. +- Skips cleanup if repository checkout is unavailable. + +## Prerequisites + +### Repository Settings + +- **Settings -> Actions -> General -> Workflow permissions**: set to **Read and write permissions**. +- **Settings -> Actions -> General**: enable **Allow GitHub Actions to create and approve pull requests**. + +### Branch Protection + +- For approvable flow: enforce PR review rules on `develop`. +- For auto-complete flow pushes to `develop`: ensure the automation actor has appropriate bypass rights in repository rules. + +## Version Format + +Accepted examples: + +- `5.2.0` +- `5.2.0-rc1` +- `5.2.0v1` +- `5.2.0.beta2` + +Rule: `major.minor.patch` digits with optional suffix. diff --git a/.github/workflows/component-release.yml b/.github/workflows/component-release.yml new file mode 100755 index 00000000..01913c2b --- /dev/null +++ b/.github/workflows/component-release.yml @@ -0,0 +1,259 @@ +name: Component Release + +on: + workflow_dispatch: + inputs: + release_version: + description: "Release version (example: 5.2.0)" + required: true + type: string + release_type: + description: "Release type" + required: true + type: choice + options: + - main + - hotfix + release_mode: + description: "Release mode (ignored for hotfix)" + required: true + type: choice + options: + - approvable + - auto-complete + source_branch: + description: "Support branch for hotfix (e.g. support/5.0). Required for hotfix, ignored for main release." + required: false + type: string + +permissions: + contents: write + pull-requests: write + +concurrency: + group: component-release-${{ github.event.inputs.release_type }}-${{ github.event.inputs.release_version }} + cancel-in-progress: false + +jobs: + release: + runs-on: ubuntu-latest + env: + RELEASE_VERSION: ${{ github.event.inputs.release_version }} + RELEASE_TYPE: ${{ github.event.inputs.release_type }} + RELEASE_MODE: ${{ github.event.inputs.release_mode }} + SOURCE_BRANCH: ${{ github.event.inputs.source_branch }} + REPO: ${{ github.repository }} + ACTOR: ${{ github.actor }} + GH_TOKEN: ${{ secrets.RDKCM_RDKE }} + + steps: + - name: Validate version format + run: | + set -euo pipefail + if ! [[ "${RELEASE_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+[A-Za-z0-9._-]*$ ]]; then + echo "Invalid version format: ${RELEASE_VERSION}" + echo "Expected format: major.minor.patch with optional suffix (e.g. 5.2.0, 5.2.0-rc1)" + exit 1 + fi + + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ssh-key: ${{ secrets.RDKCM_DEPLOY_KEY }} + + - name: Initialize cleanup flags + run: | + { + echo "CREATED_RELEASE_BRANCH=false" + echo "CREATED_HOTFIX_BRANCH=false" + echo "CREATED_TAG=false" + } >> "$GITHUB_ENV" + + - name: Install release tools + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y git-flow + npm install -g auto-changelog + + - name: Configure git identity + run: | + set -euo pipefail + git config user.name "${ACTOR}" + git config user.email "${ACTOR}@users.noreply.github.com" + + - name: Initialize git-flow + if: ${{ github.event.inputs.release_type == 'main' }} + run: | + set -euo pipefail + git fetch --prune origin + git checkout develop + git reset --hard origin/develop + git fetch origin main:main + git flow init -d + + # + # ── AUTO-COMPLETE RELEASE ── + # + - name: "Auto-complete: full release" + if: ${{ github.event.inputs.release_type == 'main' && github.event.inputs.release_mode == 'auto-complete' }} + run: | + set -euo pipefail + + if git ls-remote --exit-code --tags origin "refs/tags/${RELEASE_VERSION}" >/dev/null 2>&1; then + echo "Tag ${RELEASE_VERSION} already exists. Skipping." + exit 0 + fi + + git flow release start "${RELEASE_VERSION}" + echo "CREATED_RELEASE_BRANCH=true" >> "$GITHUB_ENV" + auto-changelog -v "${RELEASE_VERSION}" + git add CHANGELOG.md + if ! git diff --cached --quiet; then + git commit -m "${RELEASE_VERSION} release changelog updates" + fi + git flow release publish "${RELEASE_VERSION}" + git flow release finish -m "${RELEASE_VERSION} release" "${RELEASE_VERSION}" + echo "CREATED_TAG=true" >> "$GITHUB_ENV" + git push origin main + git push origin --tags + git push origin develop + git push origin --delete "release/${RELEASE_VERSION}" || true + + # + # ── APPROVABLE RELEASE ── + # + - name: "Approvable: start release and create PR" + if: ${{ github.event.inputs.release_type == 'main' && github.event.inputs.release_mode == 'approvable' }} + run: | + set -euo pipefail + + if git ls-remote --exit-code --tags origin "refs/tags/${RELEASE_VERSION}" >/dev/null 2>&1; then + echo "Tag ${RELEASE_VERSION} already exists. Skipping." + exit 0 + fi + + release_branch="release/${RELEASE_VERSION}" + if git ls-remote --exit-code --heads origin "${release_branch}" >/dev/null 2>&1; then + echo "${release_branch} already exists on remote. Skipping release start." + else + git flow release start "${RELEASE_VERSION}" + echo "CREATED_RELEASE_BRANCH=true" >> "$GITHUB_ENV" + auto-changelog -v "${RELEASE_VERSION}" + git add CHANGELOG.md + if ! git diff --cached --quiet; then + git commit -m "${RELEASE_VERSION} release changelog updates" + fi + git flow release publish "${RELEASE_VERSION}" + fi + + existing_pr=$(gh pr list --head "${release_branch}" --base develop --state open --json number -q '.[0].number') + if [ -z "${existing_pr}" ]; then + gh pr create \ + --base develop \ + --head "${release_branch}" \ + --title "Release ${RELEASE_VERSION}" \ + --body "Automated release PR for ${RELEASE_VERSION}. Approve this PR to trigger release finish." + echo "PR created. Waiting for approval to finish release." + else + echo "PR from ${release_branch} to develop already exists (#${existing_pr})." + fi + + # + # ── HOTFIX RELEASE ── + # + - name: "Hotfix: validate source branch" + if: ${{ github.event.inputs.release_type == 'hotfix' }} + run: | + set -euo pipefail + if [ -z "${SOURCE_BRANCH}" ]; then + echo "ERROR: source_branch is required for hotfix release." + exit 1 + fi + if ! [[ "${SOURCE_BRANCH}" =~ ^[A-Za-z0-9._/-]+$ ]]; then + echo "ERROR: source_branch contains invalid characters." + echo "Only alphanumeric characters, dots, underscores, hyphens, and slashes are allowed." + exit 1 + fi + + - name: "Hotfix: update support branch and push tag" + if: ${{ github.event.inputs.release_type == 'hotfix' }} + run: | + set -euo pipefail + + if git ls-remote --exit-code --tags origin "refs/tags/${RELEASE_VERSION}" >/dev/null 2>&1; then + echo "Tag ${RELEASE_VERSION} already exists. Skipping." + exit 0 + fi + + git fetch --prune origin + if ! git ls-remote --exit-code --heads origin "${SOURCE_BRANCH}" >/dev/null 2>&1; then + echo "ERROR: source branch '${SOURCE_BRANCH}' does not exist on origin." + exit 1 + fi + + git checkout -B "${SOURCE_BRANCH}" "origin/${SOURCE_BRANCH}" + + hotfix_branch="hotfix/${RELEASE_VERSION}" + if git show-ref --verify --quiet "refs/heads/${hotfix_branch}"; then + git checkout "${hotfix_branch}" + else + git checkout -b "${hotfix_branch}" "${SOURCE_BRANCH}" + echo "CREATED_HOTFIX_BRANCH=true" >> "$GITHUB_ENV" + fi + + auto-changelog -v "${RELEASE_VERSION}" + git add CHANGELOG.md + if ! git diff --cached --quiet; then + git commit -m "${RELEASE_VERSION} hotfix release" + else + echo "No CHANGELOG.md changes to commit." + fi + + git checkout "${SOURCE_BRANCH}" + git merge --no-ff "${hotfix_branch}" -m "Merge hotfix ${RELEASE_VERSION} into ${SOURCE_BRANCH}" + + git tag -a "${RELEASE_VERSION}" -m "Hotfix ${RELEASE_VERSION}" + echo "CREATED_TAG=true" >> "$GITHUB_ENV" + git push origin "${SOURCE_BRANCH}" + git push origin "${RELEASE_VERSION}" + echo "Hotfix complete: pushed ${SOURCE_BRANCH} and tag ${RELEASE_VERSION}." + + # + # ── CLEANUP ON FAILURE ── + # + - name: Cleanup on failure + if: failure() + run: | + if [ ! -d .git ]; then + echo "No repository checkout available. Skipping cleanup." + exit 0 + fi + + if [[ "${CREATED_TAG:-false}" == "true" ]] && git rev-parse -q --verify "refs/tags/${RELEASE_VERSION}" >/dev/null 2>&1; then + git tag -d "${RELEASE_VERSION}" 2>/dev/null || true + git push origin ":refs/tags/${RELEASE_VERSION}" 2>/dev/null || true + fi + + if [[ "${RELEASE_TYPE}" == "main" ]]; then + if [[ "${CREATED_RELEASE_BRANCH:-false}" == "true" ]] && git show-ref --verify --quiet "refs/heads/release/${RELEASE_VERSION}"; then + git push origin --delete "release/${RELEASE_VERSION}" 2>/dev/null || true + fi + else + if [[ "${CREATED_HOTFIX_BRANCH:-false}" == "true" ]] && git show-ref --verify --quiet "refs/heads/hotfix/${RELEASE_VERSION}"; then + git push origin --delete "hotfix/${RELEASE_VERSION}" 2>/dev/null || true + fi + fi + + - name: Workflow summary + if: always() + run: | + { + echo "## Component Release Summary" + echo "- Repository: ${REPO}" + echo "- Actor: ${ACTOR}" + echo "- Version: ${RELEASE_VERSION}" + echo "- Type: ${RELEASE_TYPE}" + echo "- Mode: ${RELEASE_MODE}" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/CHANGELOG.md b/CHANGELOG.md index 44f46bda..47c75f36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,23 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [6.0.0](https://github.com/rdkcentral/sysint/compare/5.1.0...6.0.0) + +- RDKE-1065: Automated Component Release workflow [`#553`](https://github.com/rdkcentral/sysint/pull/553) +- Merge tag '5.1.0' into develop [`4e51a4f`](https://github.com/rdkcentral/sysint/commit/4e51a4f92b24157d5062264fde85c9469242494f) + +#### [5.1.0](https://github.com/rdkcentral/sysint/compare/5.0.2...5.1.0) + +> 14 May 2026 + +- Update getPartnerProperty.sh [`#541`](https://github.com/rdkcentral/sysint/pull/541) +- 5.1.0 release changelog updates [`3ce474d`](https://github.com/rdkcentral/sysint/commit/3ce474d349b2cf3548985d4fc2f993b26d52c014) +- Merge tag '5.0.2' into develop [`1888519`](https://github.com/rdkcentral/sysint/commit/18885196ab36dc76fd5f28e87a484594a53076e1) + #### [5.0.2](https://github.com/rdkcentral/sysint/compare/5.0.1...5.0.2) +> 7 May 2026 + - RDKEMW-17800:gst-cleanup conditions when cdl_flashed_file_name is not present [`#538`](https://github.com/rdkcentral/sysint/pull/538) - RDKEMW-17726, RDKEMW-17727: Fix the Migration of RDKV-to-RDKE [`#531`](https://github.com/rdkcentral/sysint/pull/531) - RDKEMW-18159 : [develop] Corefile not getting uploaded for mutliqueue crashes [`#539`](https://github.com/rdkcentral/sysint/pull/539) @@ -14,6 +29,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Revert "RDKEMW-10725:gstreamer-cleanup conditions when cdl_flashed_file_name …" [`#526`](https://github.com/rdkcentral/sysint/pull/526) - RDKEMW-17619 : [develop] OCDM_WVMediaKey and OCDM_SaThread changes in sysint [`#522`](https://github.com/rdkcentral/sysint/pull/522) - RDKEMW-10725:gstreamer-cleanup conditions when cdl_flashed_file_name is missing [`#511`](https://github.com/rdkcentral/sysint/pull/511) +- Sysint 5.0.2 release changelog updates [`0e05ed7`](https://github.com/rdkcentral/sysint/commit/0e05ed7d7dd01718d723584f4da02e20ace397de) - Merge tag '5.0.1' into develop [`5d44360`](https://github.com/rdkcentral/sysint/commit/5d443603d5a7b803f4f45af68f0178e057a21968) #### [5.0.1](https://github.com/rdkcentral/sysint/compare/5.0.0...5.0.1)