|
| 1 | +# .github/workflows/create-release-branch-v1.yml |
| 2 | +name: Create Feature Branches in Repos v1 |
| 3 | + |
| 4 | +on: |
| 5 | + # This workflow can be run manually from the Actions tab. |
| 6 | + workflow_dispatch: |
| 7 | + inputs: |
| 8 | + BRANCH_NAME: |
| 9 | + description: 'The name of the new feature/release branch to create, e.g. 18.0-fr3 .' |
| 10 | + required: true |
| 11 | + type: string |
| 12 | + NEW_VERSION: |
| 13 | + description: 'The new version to set in the Makefile on the main/source branch, e.g. 0.5 .' |
| 14 | + required: true |
| 15 | + type: string |
| 16 | + FORCE_BUMP_BRANCHES: |
| 17 | + description: 'The new list of branches for the force-bump-branches.yaml workflow, e.g. ["main", "18.0-fr3"] .' |
| 18 | + required: true |
| 19 | + type: string |
| 20 | + DRY_RUN: |
| 21 | + description: 'Test Run! Run without pushing any changes. Disable to apply changes.' |
| 22 | + required: true |
| 23 | + type: boolean |
| 24 | + default: true |
| 25 | + |
| 26 | +jobs: |
| 27 | + create-branches: |
| 28 | + runs-on: ubuntu-latest |
| 29 | + env: |
| 30 | + # Use current org as the ORG_NAME |
| 31 | + ORG_NAME: ${{ github.repository_owner }} |
| 32 | + # --- CONFIGURATION FROM INPUTS --- |
| 33 | + BRANCH_NAME: ${{ inputs.BRANCH_NAME }} |
| 34 | + NEW_VERSION: ${{ inputs.NEW_VERSION }} |
| 35 | + FORCE_BUMP_BRANCHES: ${{ inputs.FORCE_BUMP_BRANCHES }} |
| 36 | + DRY_RUN: ${{ inputs.DRY_RUN }} |
| 37 | + # 'The name of the version variable in the Makefile.' |
| 38 | + VERSION_VARIABLE: 'VERSION' |
| 39 | + # 'The name of the branch variable in the Makefile.' |
| 40 | + BRANCH_VARIABLE: 'BRANCH' |
| 41 | + # 'The name of the OpenStack K8s tag variable in the Makefile (for install_yamls).' |
| 42 | + OPENSTACK_K8S_TAG_VARIABLE: 'OPENSTACK_K8S_TAG' |
| 43 | + # 'The branch that must exist in a repo for it to be processed.' |
| 44 | + TRIGGER_BRANCH: 'olive' |
| 45 | + # 'The branch to create the new branch from.' |
| 46 | + SOURCE_BRANCH: 'main' |
| 47 | + steps: |
| 48 | + - name: Generate a token from the GitHub App |
| 49 | + uses: actions/create-github-app-token@v2 |
| 50 | + id: app-token |
| 51 | + with: |
| 52 | + app-id: ${{ secrets.APP_ID }} |
| 53 | + private-key: ${{ secrets.APP_PRIVATE_KEY }} |
| 54 | + # request an app token for the org instead of just the repo |
| 55 | + owner: ${{ env.ORG_NAME }} |
| 56 | + |
| 57 | + - name: Get GitHub App User ID |
| 58 | + id: get-user-id |
| 59 | + run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" |
| 60 | + env: |
| 61 | + GH_TOKEN: ${{ steps.app-token.outputs.token }} |
| 62 | + |
| 63 | + - name: Install yq |
| 64 | + run: | |
| 65 | + sudo wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq |
| 66 | + sudo chmod +x /usr/bin/yq |
| 67 | + echo "yq version $(yq --version) installed." |
| 68 | +
|
| 69 | + - name: Validate Inputs |
| 70 | + run: | |
| 71 | + set -e |
| 72 | + if ! echo "${FORCE_BUMP_BRANCHES}" | yq -e 'tag == "!!seq"' >/dev/null 2>&1; then |
| 73 | + echo "Error: The 'FORCE_BUMP_BRANCHES' input "${FORCE_BUMP_BRANCHES}" is not a valid YAML array." |
| 74 | + echo "Please provide it in the format [\"item1\", \"item2\"]." |
| 75 | + exit 1 |
| 76 | + fi |
| 77 | + |
| 78 | + if [[ -z "$BRANCH_NAME" ]] || [[ -z "$NEW_VERSION" ]]; then |
| 79 | + echo "Error: Required inputs BRANCH_NAME or NEW_VERSION were not provided." |
| 80 | + exit 1 |
| 81 | + fi |
| 82 | + echo "Inputs are valid." |
| 83 | + |
| 84 | + - name: Checkout code (for the workflow itself) |
| 85 | + uses: actions/checkout@v4 |
| 86 | + with: |
| 87 | + token: ${{ steps.app-token.outputs.token }} |
| 88 | + |
| 89 | + - name: Create Function Library |
| 90 | + run: | |
| 91 | + set -e |
| 92 | +
|
| 93 | + # Use a heredoc to write a multi-line script file. |
| 94 | + # This file will contain all your reusable functions. |
| 95 | + cat <<'EOF' > workflow_functions.sh |
| 96 | + #!/bin/bash |
| 97 | +
|
| 98 | + # A function to handle committing and pushing changes. |
| 99 | + # Arguments: 1: Commit Message, 2: File(s) to add, 3: Branch to push |
| 100 | + commit_and_push() { |
| 101 | + local commit_message="$1" |
| 102 | + local files_to_add="$2" |
| 103 | + local branch_to_push="$3" |
| 104 | +
|
| 105 | + if ! git diff --quiet -- $files_to_add; then |
| 106 | + echo "Showing changes for branch '${branch_to_push}':" |
| 107 | + git diff -- $files_to_add |
| 108 | + if [ "$DRY_RUN" = "true" ]; then |
| 109 | + echo "DRY RUN: Commit '${commit_message}' not created for branch '${branch_to_push}'." |
| 110 | + else |
| 111 | + git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]' |
| 112 | + git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com' |
| 113 | + git add $files_to_add |
| 114 | + git commit -m "$commit_message" |
| 115 | + echo "Pushing changes to ${branch_to_push}..." |
| 116 | + git push origin ${branch_to_push} |
| 117 | + fi |
| 118 | + else |
| 119 | + echo "No changes detected in '${files_to_add}'. Nothing to commit." |
| 120 | + fi |
| 121 | + } |
| 122 | +
|
| 123 | + # Updates a value in a YAML file, automatically detecting the type of the existing node. |
| 124 | + # Arguments: 1: File Path, 2: YQ Target Path, 3: New Value |
| 125 | + update_yaml_value() { |
| 126 | + local file_path="$1" |
| 127 | + local yq_path="$2" |
| 128 | + local new_value="$3" |
| 129 | +
|
| 130 | + if [ ! -f "${file_path}" ]; then echo "Error: File '${file_path}' not found. Skipping."; return; fi |
| 131 | + if ! yq -e "(${yq_path}) != null" "${file_path}" >/dev/null; then echo "Error: Path '${yq_path}' not found in '${file_path}'."; return; fi |
| 132 | + |
| 133 | + # Get the tag of the existing node to determine its type |
| 134 | + local current_tag |
| 135 | + current_tag=$(yq e "(${yq_path}) | tag" "${file_path}") |
| 136 | +
|
| 137 | + if [ "$current_tag" = "!!seq" ]; then |
| 138 | + # It's an array, treat new value as a literal so yq can parse it |
| 139 | + echo "Path '${yq_path}' is an array. Replacing with new value: ${new_value}" |
| 140 | + yq -i "${yq_path} = ${new_value}" "${file_path}" |
| 141 | + else |
| 142 | + # It's a scalar (string, int, etc.), treat new value as a string |
| 143 | + echo "Path '${yq_path}' is a scalar. Updating from '${current_value}' to '${new_value}'..." |
| 144 | + yq -i "${yq_path} = \"${new_value}\"" "${file_path}" |
| 145 | + fi |
| 146 | + } |
| 147 | +
|
| 148 | + # Reads a value from a YAML file using a yq path. |
| 149 | + # Arguments: 1: File Path, 2: YQ Target Path |
| 150 | + get_yaml_value() { |
| 151 | + local file_path="$1" |
| 152 | + local yq_path="$2" |
| 153 | +
|
| 154 | + # Check if the file exists |
| 155 | + if [ ! -f "${file_path}" ]; then echo "Error: File '${file_path}' not found. Skipping." >&2; return; fi |
| 156 | + if ! yq -e "(${yq_path}) != null" "${file_path}" >/dev/null; then echo "Error: Path '${yq_path}' not found in '${file_path}'."; return; fi |
| 157 | +
|
| 158 | + # If checks pass, get and echo the value |
| 159 | + yq e "${yq_path}" "${file_path}" |
| 160 | + } |
| 161 | + EOF |
| 162 | +
|
| 163 | + - name: Create release branches and update the release branch |
| 164 | + env: |
| 165 | + # Use the token generated from the GitHub App |
| 166 | + GH_TOKEN: ${{ steps.app-token.outputs.token }} |
| 167 | + # GH_TOKEN: ${{ secrets.ORG_ADMIN_PAT }} |
| 168 | + run: | |
| 169 | + set -e # Exit immediately if a command exits with a non-zero status. |
| 170 | + source ./workflow_functions.sh |
| 171 | + |
| 172 | + if [ "$DRY_RUN" = "true" ]; then |
| 173 | + echo "!!! --- Running in DRY RUN mode. No changes will be pushed. --- !!!" |
| 174 | + fi |
| 175 | + |
| 176 | + echo "Running with the following configuration:" |
| 177 | + echo "Organization: ${ORG_NAME}" |
| 178 | + echo "Feature Branch Name: ${BRANCH_NAME}" |
| 179 | + echo "New Version on main: ${NEW_VERSION}" |
| 180 | + echo "New FORCE_BUMP_BRANCHES: ${FORCE_BUMP_BRANCHES}" |
| 181 | + echo "Trigger Branch: ${TRIGGER_BRANCH}" |
| 182 | + echo "Source Branch: ${SOURCE_BRANCH}" |
| 183 | + echo "-------------------------------------------" |
| 184 | + |
| 185 | + echo "Searching for repositories in organization '${ORG_NAME}'..." |
| 186 | + repos=$(gh repo list $ORG_NAME --limit 1000 --json name --jq '.[] | .name') |
| 187 | + |
| 188 | + if [ -z "$repos" ]; then |
| 189 | + echo "No repositories found in the organization or permissions are missing." |
| 190 | + exit 1 |
| 191 | + fi |
| 192 | + |
| 193 | + created_repos="" |
| 194 | + |
| 195 | + for repo_name in $repos; do |
| 196 | + echo "--- Checking REPOSITORY: ${repo_name} ---" |
| 197 | + |
| 198 | + if [[ "$repo_name" == "rabbitmq-cluster-operator" ]]; then |
| 199 | + continue |
| 200 | + fi |
| 201 | + |
| 202 | + if gh api "repos/${ORG_NAME}/${repo_name}/branches/${TRIGGER_BRANCH}" >/dev/null 2>&1; then |
| 203 | + echo "✅ Trigger branch '${TRIGGER_BRANCH}' found in ${repo_name}." |
| 204 | + |
| 205 | + TEMP_DIR=$(mktemp -d) |
| 206 | + echo "Cloning repository into ${TEMP_DIR}..." |
| 207 | + git clone "https://x-access-token:${GH_TOKEN}@github.com/${ORG_NAME}/${repo_name}.git" "$TEMP_DIR" |
| 208 | + |
| 209 | + cd "$TEMP_DIR" |
| 210 | + |
| 211 | + if ! git show-ref --verify --quiet "refs/remotes/origin/${SOURCE_BRANCH}"; then |
| 212 | + echo "⚠️ Source branch '${SOURCE_BRANCH}' does not exist in ${repo_name}. Skipping." |
| 213 | + cd .. && rm -rf "$TEMP_DIR" |
| 214 | + continue |
| 215 | + fi |
| 216 | + |
| 217 | + if git show-ref --verify --quiet "refs/remotes/origin/${BRANCH_NAME}"; then |
| 218 | + echo "ℹ️ Branch '${BRANCH_NAME}' already exists in ${repo_name}. Skipping this repository." |
| 219 | + cd .. && rm -rf "$TEMP_DIR" |
| 220 | + continue |
| 221 | + fi |
| 222 | + |
| 223 | + echo "Creating branch '${BRANCH_NAME}' from '${SOURCE_BRANCH}'..." |
| 224 | + git checkout ${SOURCE_BRANCH} |
| 225 | + git checkout -b ${BRANCH_NAME} |
| 226 | + |
| 227 | + if [ -f "Makefile" ]; then |
| 228 | + echo "Makefile found. Checking for variables to update on new branch..." |
| 229 | + if grep -qE "^${BRANCH_VARIABLE}\s*(\?=|=)" Makefile; then |
| 230 | + sed -i -E "s/^(${BRANCH_VARIABLE}\s*(\?=|=)\s*).*/\1${BRANCH_NAME}/" Makefile |
| 231 | + fi |
| 232 | + if [[ "$repo_name" == "install_yamls" ]]; then |
| 233 | + if grep -qE "^${OPENSTACK_K8S_TAG_VARIABLE}\s*(\?=|=)" Makefile; then |
| 234 | + sed -i -E "s/^((${OPENSTACK_K8S_TAG_VARIABLE})\s*(\?=|=)\s*).*/\1${BRANCH_NAME}-latest/" Makefile |
| 235 | + fi |
| 236 | + fi |
| 237 | + commit_and_push "[${BRANCH_NAME}] Update Makefile for ${BRANCH_NAME}" "Makefile" "${BRANCH_NAME}" |
| 238 | + else |
| 239 | + echo "No Makefile found. No changes will be committed to new branch." |
| 240 | + fi |
| 241 | + |
| 242 | + created_repos+="- ${repo_name}\n" |
| 243 | + |
| 244 | + cd .. && rm -rf "$TEMP_DIR" |
| 245 | + else |
| 246 | + echo "❌ Trigger branch '${TRIGGER_BRANCH}' not found in ${repo_name}. Skipping." |
| 247 | + fi |
| 248 | + done |
| 249 | + |
| 250 | + echo "-------------------------------------------" |
| 251 | + if [ "$DRY_RUN" = "true" ]; then |
| 252 | + echo "Summary of repos where the new branch ${BRANCH_NAME} would have been created (DRY RUN):" |
| 253 | + else |
| 254 | + echo "Summary of repos where the new branch ${BRANCH_NAME} was created:" |
| 255 | + fi |
| 256 | + |
| 257 | + if [[ -n "$created_repos" ]]; then |
| 258 | + echo -e "$created_repos" |
| 259 | + else |
| 260 | + echo "No new branches were created." |
| 261 | + fi |
| 262 | + |
| 263 | + - name: Update openstack-operator main branch |
| 264 | + env: |
| 265 | + # Use the token generated from the GitHub App |
| 266 | + GH_TOKEN: ${{ steps.app-token.outputs.token }} |
| 267 | + run: | |
| 268 | + set -e # Exit immediately if a command exits with a non-zero status. |
| 269 | + source ./workflow_functions.sh |
| 270 | + |
| 271 | + echo "--- Checking REPOSITORY: openstack-k8s-operators-ci ---" |
| 272 | + TEMP_DIR=$(mktemp -d) |
| 273 | + echo "Cloning repository into ${TEMP_DIR}..." |
| 274 | + git clone "https://x-access-token:${GH_TOKEN}@github.com/${ORG_NAME}/openstack-operator.git" "$TEMP_DIR" |
| 275 | + cd "$TEMP_DIR" |
| 276 | + |
| 277 | + # Update workflows on the main branch |
| 278 | + git checkout main |
| 279 | + git pull origin main |
| 280 | + |
| 281 | + # change to the Makefile |
| 282 | + if [ -f "Makefile" ]; then |
| 283 | + FULL_MAKEFILE_VERSION="${NEW_VERSION}.0" |
| 284 | + if grep -qE "^${VERSION_VARIABLE}\s*(:=|\?=|=)" Makefile; then |
| 285 | + sed -i -E "s/^(${VERSION_VARIABLE}\s*(:=|\?=|=)).*/\1 ${FULL_MAKEFILE_VERSION}/" Makefile |
| 286 | + commit_and_push "Bump Makefile version to ${FULL_MAKEFILE_VERSION}" "Makefile" "${SOURCE_BRANCH}" |
| 287 | + else |
| 288 | + echo "Variable '${VERSION_VARIABLE}' not found. No changes pushed to ${SOURCE_BRANCH}." |
| 289 | + fi |
| 290 | + else |
| 291 | + echo "No Makefile found. No changes pushed to ${SOURCE_BRANCH}." |
| 292 | + fi |
| 293 | + |
| 294 | + # change to hack/fake_minor_update.sh |
| 295 | + if [ -f "hack/fake_minor_update.sh" ]; then |
| 296 | + if grep -qE "^${VERSION_VARIABLE}=" hack/fake_minor_update.sh; then |
| 297 | + sed -i -E "s/^(${VERSION_VARIABLE}=).*/\1${NEW_VERSION}/" hack/fake_minor_update.sh |
| 298 | + commit_and_push "Bump fake_minor_update.sh version to ${NEW_VERSION}" "hack/fake_minor_update.sh" "${SOURCE_BRANCH}" |
| 299 | + else |
| 300 | + echo "Variable '${VERSION_VARIABLE}' not found. No changes pushed to ${SOURCE_BRANCH}." |
| 301 | + fi |
| 302 | + else |
| 303 | + echo "No hack/fake_minor_update.sh found. No changes pushed to ${SOURCE_BRANCH}." |
| 304 | + fi |
| 305 | + |
| 306 | + # changes to .github/workflows/catalog-openstack-operator-upgrades.yaml |
| 307 | + WORKFLOW_FILE=".github/workflows/catalog-openstack-operator-upgrades.yaml" |
| 308 | + if [ -f "${WORKFLOW_FILE}" ]; then |
| 309 | + # 1. Find the index of the step to update |
| 310 | + index=$(yq '.jobs.build-catalog.steps | to_entries | .[] | select(.value.name == "Create the catalog index") | .key' ${WORKFLOW_FILE}) |
| 311 | + |
| 312 | + # 2. Check if an index was found |
| 313 | + if [ -z "$index" ]; then |
| 314 | + echo "Step 'Create the catalog index' not found." |
| 315 | + exit 1 |
| 316 | + fi |
| 317 | + |
| 318 | + echo "Found 'Create the catalog index' step at index: $index" |
| 319 | + CURRENT_VERSION=$(get_yaml_value "${WORKFLOW_FILE}" ".jobs.build-catalog.steps[$index].env.MAIN_VERSION") |
| 320 | + FULL_NEW_VERSION="${NEW_VERSION}.0" |
| 321 | + # 3. Use the index to update the 'image' field in place (-i) |
| 322 | + update_yaml_value "${WORKFLOW_FILE}" ".jobs.build-catalog.steps[$index].env.MAIN_VERSION" "${FULL_NEW_VERSION}" |
| 323 | + update_yaml_value "${WORKFLOW_FILE}" ".jobs.build-catalog.steps[$index].env.FEATURE_RELEASE_VERSION" "${CURRENT_VERSION}" |
| 324 | + update_yaml_value "${WORKFLOW_FILE}" ".jobs.build-catalog.steps[$index].env.FEATURE_RELEASE_BRANCH" "${BRANCH_NAME}" |
| 325 | + commit_and_push "Bump version in ${WORKFLOW_FILE} to ${FULL_NEW_VERSION}" "${WORKFLOW_FILE}" "${SOURCE_BRANCH}" |
| 326 | + else |
| 327 | + echo "No ${WORKFLOW_FILE} found. No changes pushed to ${SOURCE_BRANCH}." |
| 328 | + fi |
| 329 | + |
| 330 | + cd .. && rm -rf "$TEMP_DIR" |
| 331 | + |
| 332 | + - name: Update openstack-k8s-operators-ci workflows |
| 333 | + env: |
| 334 | + # Use the token generated from the GitHub App |
| 335 | + GH_TOKEN: ${{ steps.app-token.outputs.token }} |
| 336 | + run: | |
| 337 | + set -e # Exit immediately if a command exits with a non-zero status. |
| 338 | + source ./workflow_functions.sh |
| 339 | +
|
| 340 | + echo "--- Checking REPOSITORY: openstack-k8s-operators-ci ---" |
| 341 | + TEMP_DIR=$(mktemp -d) |
| 342 | + echo "Cloning repository into ${TEMP_DIR}..." |
| 343 | + git clone "https://x-access-token:${GH_TOKEN}@github.com/${ORG_NAME}/openstack-k8s-operators-ci.git" "$TEMP_DIR" |
| 344 | + cd "$TEMP_DIR" |
| 345 | +
|
| 346 | + # Update workflows on the main branch |
| 347 | + git checkout main |
| 348 | + git pull origin main |
| 349 | +
|
| 350 | + update_yaml_value ".github/workflows/release-branch-sync.yaml" ".on.workflow_call.inputs.source_branch.default" "${BRANCH_NAME}" |
| 351 | + update_yaml_value ".github/workflows/force-bump-branches.yaml" ".jobs.trigger-jobs.strategy.matrix.branch" "${FORCE_BUMP_BRANCHES}" |
| 352 | +
|
| 353 | + # Commit and push changes if any were made |
| 354 | + commit_and_push "ci: Update workflow defaults for ${BRANCH_NAME}" ".github/workflows/" "${SOURCE_BRANCH}" |
| 355 | +
|
| 356 | + cd .. && rm -rf "$TEMP_DIR" |
| 357 | + echo "Workflow finished." |
| 358 | +
|
| 359 | + retag-and-push-rabbitmq-cluster-operator-index: |
| 360 | + if: inputs.DRY_RUN == false |
| 361 | + needs: create-branches |
| 362 | + uses: ./.github/workflows/rabbitmq-cluster-operator-index-feature-tag.yaml |
| 363 | + with: |
| 364 | + new_tag: "${{ inputs.BRANCH_NAME }}-latest" |
| 365 | + secrets: |
| 366 | + # Pass secrets as before |
| 367 | + IMAGENAMESPACE: ${{ secrets.IMAGENAMESPACE }} |
| 368 | + QUAY_USERNAME: ${{ secrets.QUAY_USERNAME }} |
| 369 | + QUAY_PASSWORD: ${{ secrets.QUAY_PASSWORD }} |
0 commit comments