diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100755 index 00000000..6fc83552 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,105 @@ +# 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: 1.0.0, 1.0.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/1.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. +4. Deletes remote release/ branch after successful finish. + +Main release - approvable +1. Validates version format. +2. Runs git flow release start -> changelog -> release publish. +3. Creates a PR: release/ -> main. +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 main. + +#### 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. Deletes remote release/ branch. +6. Closes the release PR. + +## Authentication and Secrets + +Both workflows use: + +- RDKCM_DEPLOY_KEY: SSH private key used by checkout and git push operations. +- github.token: built-in ephemeral GitHub Actions token used by gh API/CLI calls. + +Why github.token: + +- It is automatically provided for each workflow run. +- It avoids introducing a custom repository secret for API access. +- It works with the workflow permissions already declared in the job. + +## 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 main. +- For auto-complete flow pushes to develop: ensure the automation actor has appropriate bypass rights in repository rules. + +## Version Format + +Accepted examples: + +- 1.0.0 +- 1.0.0-rc1 +- 1.0.0v1 +- 1.0.0.beta2 + +Rule: major.minor.patch digits with optional suffix. 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..c7f35a02 --- /dev/null +++ b/.github/workflows/component-release-finish-on-approval.yml @@ -0,0 +1,115 @@ +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 == 'main' && + startsWith(github.event.pull_request.head.ref, 'release/') + runs-on: ubuntu-latest + env: + REPO: ${{ github.repository }} + ACTOR: ${{ github.event.review.user.login }} + GH_TOKEN: ${{ github.token }} + 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 + + 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 + + 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 + + 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}" + + git flow init -d + + 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.yml b/.github/workflows/component-release.yml new file mode 100755 index 00000000..e2aec759 --- /dev/null +++ b/.github/workflows/component-release.yml @@ -0,0 +1,247 @@ +name: Component Release + +on: + workflow_dispatch: + inputs: + release_version: + description: "Release version (example: 1.0.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/1.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: ${{ github.token }} + + 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. 1.0.0, 1.0.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 + + - 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 + + - 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 main --state open --json number -q '.[0].number') + if [ -z "${existing_pr}" ]; then + gh pr create \ + --base main \ + --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 main already exists (#${existing_pr})." + fi + + - 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}." + + - 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"