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..0b34fcca --- /dev/null +++ b/.github/workflows/component-release-finish-on-approval.yml @@ -0,0 +1,133 @@ +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: comcast-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: Verify approver is a maintainer + id: auth + run: | + set -euo pipefail + role=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" -q '.role_name' | tr '[:upper:]' '[:lower:]') + echo "Approver '${ACTOR}' role: ${role}" + if [[ "${role}" == "admin" || "${role}" == "maintain" ]]; then + echo "is_maintainer=true" >> "$GITHUB_OUTPUT" + else + echo "Approver is not a maintainer. Skipping release finish." + echo "is_maintainer=false" >> "$GITHUB_OUTPUT" + fi + + - name: Check approval status + if: steps.auth.outputs.is_maintainer == 'true' + 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.auth.outputs.is_maintainer == 'true' && steps.approvals.outputs.should_finish == 'true' + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.RDKCM_RDKE }} + + - name: Install release tools + if: steps.auth.outputs.is_maintainer == 'true' && 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.auth.outputs.is_maintainer == 'true' && 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.auth.outputs.is_maintainer == 'true' && 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 + + - name: Close the release PR + if: steps.auth.outputs.is_maintainer == 'true' && 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.yml b/.github/workflows/component-release.yml new file mode 100755 index 00000000..8fd30f31 --- /dev/null +++ b/.github/workflows/component-release.yml @@ -0,0 +1,168 @@ +name: Component Release + +on: + workflow_dispatch: + inputs: + release_version: + description: "Release version (example: 5.2.0)" + required: true + type: string + release_mode: + description: "Release mode" + required: true + type: choice + options: + - approvable + - auto-complete + +permissions: + contents: write + pull-requests: write + +jobs: + release: + runs-on: comcast-ubuntu-latest + env: + RELEASE_VERSION: ${{ github.event.inputs.release_version }} + RELEASE_MODE: ${{ github.event.inputs.release_mode }} + 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: Authorize auto-complete (maintainers only) + if: ${{ github.event.inputs.release_mode == 'auto-complete' }} + run: | + set -euo pipefail + role=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" -q '.role_name' | tr '[:upper:]' '[:lower:]') + echo "Actor '${ACTOR}' role: ${role}" + case "${role}" in + admin|maintain) + echo "Authorization successful." + ;; + *) + echo "ERROR: Only maintainers/owners can run auto-complete releases." + exit 1 + ;; + esac + + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.RDKCM_RDKE }} + + - 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 + 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_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}" + 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}" + git push origin main + git push origin --tags + git push origin develop + + # + # ── APPROVABLE RELEASE ── + # + - name: "Approvable: start release and create PR" + if: ${{ 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}" + 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 + + # + # ── CLEANUP ON FAILURE ── + # + - name: Cleanup on failure + if: failure() + run: | + git tag -d "${RELEASE_VERSION}" 2>/dev/null || true + git push origin ":refs/tags/${RELEASE_VERSION}" 2>/dev/null || true + git push origin --delete "release/${RELEASE_VERSION}" 2>/dev/null || true + + - name: Workflow summary + if: always() + run: | + { + echo "## Component Release Summary" + echo "- Repository: ${REPO}" + echo "- Actor: ${ACTOR}" + echo "- Version: ${RELEASE_VERSION}" + echo "- Mode: ${RELEASE_MODE}" + } >> "$GITHUB_STEP_SUMMARY"