diff --git a/.github/actions/sync-branches/README.md b/.github/actions/sync-branches/README.md new file mode 100644 index 0000000..8f5ed1d --- /dev/null +++ b/.github/actions/sync-branches/README.md @@ -0,0 +1,435 @@ +# Sync Branches Action + +Automated branch synchronization that keeps a target branch (for example `main`) in sync with a source branch (for example `develop`). + +## Features + +- **Automatic triggers**: Runs on every push to source branch or manual dispatch +- **Clean merges**: Direct push to target branch when no conflicts exist +- **Conflict handling**: Opens or updates fallback PR and fallback issue artifacts for manual resolution +- **Idempotent**: Skips sync if source is already in target (no unnecessary merges) +- **Safer token policy**: Sync operations prefer the configured PAT and fall back to `github.token` +- **Policy-compliant fallback titles**: Fallback PR titles include a Jira key (derived or override) +- **Customizable**: Configure branches, labels, reviewers, and commit identity + +## Consumption Modes + +This automation is intentionally distributed in two modes so internal (`rdk-e`) and OSS (`rdkcentral`) repositories can consume the same behavior using different delivery models. + +## Workflow Placement Rule + +Always install the workflow YAML at: +- `.github/workflows/sync-develop-to-main.yml` + +Do not place the workflow file under `.github/actions/`. +GitHub only auto-discovers workflow files in `.github/workflows/`. + +Naming note: +- The reusable action metadata file must remain `action.yml` inside `actions/sync-branches/`. +- `sync-develop-to-main.yml` is the consumer workflow filename under `.github/workflows/`. + +### Mode A: By Reference (RDK-E/Internal) + +How it works: +- Consumer workflow references the shared action directly: `uses: rdk-e/app-gateway-automation/actions/sync-branches@actions-v1`. +- The workflow file lives in the consumer repo, while action logic is executed from the shared release tag. + +Why this mode exists: +- Centralized ownership and rollout from one repository. +- No vendored action files in consumer repositories. +- Best fit for internal repositories that can access `rdk-e` and internal runners. + +Tradeoffs: +- Requires cross-repository access to `rdk-e/app-gateway-automation`. +- Behavior follows the moved major tag (`actions-v1`) release lifecycle. + +### Mode B: Install Mode (OSS/Vendored) + +How it works: +- Installer/wrapper copies managed files into the consumer repository. +- Consumer workflow calls the local action path: `uses: ./.github/actions/sync-branches`. + +Why this mode exists: +- Works when OSS repositories cannot consume private `rdk-e` references. +- Keeps automation self-contained and auditable in the target repository. +- Enables explicit, versioned upgrades via release bundle + lock file. + +Tradeoffs: +- Consumer repositories carry managed automation files. +- Upgrades require running installer/wrapper and merging the resulting change. + +## Mode A Quick Start (By Reference) + +### 1. Copy The Consumer Template + +Copy `actions/sync-branches/consumer-template.yml` into your consumer repo as `.github/workflows/sync-develop-to-main.yml`. + +Placement check: +- Correct: `.github/workflows/sync-develop-to-main.yml` +- Incorrect: `.github/actions/sync-develop-to-main.yml` + +The default template uses: +- `rdk-e/app-gateway-automation/actions/sync-branches@actions-v1` +- `runs-on: comcast-ubuntu-latest` +- `source_branch: develop` +- `target_branch: main` + +### 2. Optional: Add Token Secrets + +For RDK-E closed-source repos, use `RDKE_GITHUB_TOKEN` as the standard secret. +The template also supports `SEMANTIC_RELEASE_TOKEN` as a fallback. + +Token precedence in template workflows is: +1. `RDKE_GITHUB_TOKEN` +2. `SEMANTIC_RELEASE_TOKEN` +3. `github.token` + +Required PAT scopes: +- `contents:write` +- `pull-requests:write` +- `issues:write` + +### 3. Example Consumer Workflow + +```yaml +name: Sync develop to main + +on: + push: + branches: [develop] + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + issues: write +jobs: + sync: + runs-on: comcast-ubuntu-latest + steps: + - uses: rdk-e/app-gateway-automation/actions/sync-branches@actions-v1 + with: + source_branch: ${{ github.event.inputs.source_branch || 'develop' }} + target_branch: ${{ github.event.inputs.target_branch || 'main' }} + token: ${{ secrets.RDKE_GITHUB_TOKEN || secrets.SEMANTIC_RELEASE_TOKEN || github.token }} +``` + +### 4. Optional: Generate From Installer + +You can generate a repo-specific workflow from `tools/install-workflow.sh`: + +Default secret selection is automatic by target repo origin: +- `rdkcentral/*` repos: `SEMANTIC_RELEASE_TOKEN` +- other repos: `RDKE_GITHUB_TOKEN` +- `--secret-name` overrides either default + +```bash +./tools/install-workflow.sh \ + --workflow sync-develop-to-main \ + --repo-dir ../firebolt-entos-apis \ + --source-branch develop \ + --target-branch main \ + --runner comcast-ubuntu-latest \ + --open-pr +``` + +This writes `.github/workflows/sync-develop-to-main.yml` in the target repo. + +### 5. Single Command For Multiple RDK-E Consumer Repos + +```bash +SCRIPT=./tools/install-workflow.sh +for repo in ../firebolt-entos-apis ../firebolt-players ../firebolt-entos-runtime-apis; do + "$SCRIPT" --workflow sync-develop-to-main --repo-dir "$repo" --runner comcast-ubuntu-latest --open-pr +done +``` + +## Mode B Quick Start (Install Mode) + +For OSS repositories that cannot directly consume private reusable actions, use the bundle installer flow: + +The bundle installer renders the workflow to use the local action path: + +- `uses: ./.github/actions/sync-branches` + +Important: +- The workflow file still belongs in `.github/workflows/`. +- Only the reusable action implementation lives in `.github/actions/sync-branches/`. + +1. Download release assets from `app-gateway-automation`: +- `sync-branches-.tar.gz` +- `sync-branches-.manifest.yaml` +- `sync-branches-.sha256` + +2. Install into the OSS repo: + +```bash +./tools/install-automation-bundle.sh install \ + --repo-dir /path/to/oss-repo \ + --bundle /path/to/sync-branches-.tar.gz +``` + +For `rdkcentral/*` repos, use `SEMANTIC_RELEASE_TOKEN` as the secret name (unless your bundle manifest specifies another default). + +3. Check current state: + +```bash +./tools/install-automation-bundle.sh status \ + --repo-dir /path/to/oss-repo \ + --bundle /path/to/sync-branches-.tar.gz +``` + +4. Update to a newer bundle: + +```bash +./tools/install-automation-bundle.sh update \ + --repo-dir /path/to/oss-repo \ + --bundle /path/to/sync-branches-.tar.gz +``` + +5. Optional: One-command OSS onboarding wrapper (install/update + branch + commit + PR): + +```bash +./tools/onboard-oss-sync.sh onboard \ + --repo-dir /path/to/oss-repo \ + --bundle /path/to/sync-branches-.tar.gz +``` + +Managed files: +- `.github/actions/sync-branches/` +- `.github/workflows/sync-develop-to-main.yml` +- `.github/automation-install.lock.json` + +Drift policy: +- Managed file drift is blocked by default. +- Use `--force` to overwrite managed drift. + +## Release Then Onboard Consumers + +For fastest rollout, use this order: + +1. Run `.github/workflows/release-actions.yml` on `main` to publish `actions-vX.Y.Z`. +2. Confirm the floating major tag (`actions-v1`) moved to the new release. +3. Choose consumption mode per repo: + - Mode A (RDK-E): install consumer workflow pinned to `@actions-v1`. + - Mode B (OSS): install or update managed bundle files. +4. In each consumer repo, ensure permissions include `contents: write`, `pull-requests: write`, and `issues: write`. +5. Trigger `workflow_dispatch` once to validate token and branch settings. + +## Usage Examples (Mode A By Reference) + +### Example 1: Simple develop -> main sync + +```yaml +jobs: + sync: + runs-on: comcast-ubuntu-latest + steps: + - uses: rdk-e/app-gateway-automation/actions/sync-branches@actions-v1 + with: + token: ${{ github.token }} +``` + +### Example 2: Custom branches with token + +```yaml +jobs: + sync: + runs-on: comcast-ubuntu-latest + steps: + - uses: rdk-e/app-gateway-automation/actions/sync-branches@actions-v1 + with: + source_branch: staging + target_branch: production + token: ${{ secrets.MY_PAT }} +``` + +### Example 3: Custom PR behavior + +```yaml +jobs: + sync: + runs-on: comcast-ubuntu-latest + steps: + - uses: rdk-e/app-gateway-automation/actions/sync-branches@actions-v1 + with: + assign_reviewers: "@platform-team,@devops-team" + pr_labels: "auto-merge,requires-review" + git_user_name: "Release Bot" + git_user_email: "releases@company.com" + token: ${{ secrets.RDKE_GITHUB_TOKEN }} +``` + +## Inputs + +| Input | Default | Description | +|-------|---------|-------------| +| `source_branch` | `develop` | Branch to sync from | +| `target_branch` | `main` | Branch to sync to | +| `git_user_name` | `github-actions[bot]` | Committer name for merge commits | +| `git_user_email` | `github-actions[bot]@users.noreply.github.com` | Committer email | +| `pr_branch_prefix` | `auto-sync` | Prefix for fallback PR branches | +| `assign_reviewers` | `` | Comma-separated team/user handles for PR assignment | +| `fallback_pr_jira_key` | `` | Optional Jira key override for fallback PR title (e.g. `RDKEMW-12345`) | +| `pr_labels` | `auto-sync,needs-manual-merge` | Comma-separated labels for fallback PR | +| `issue_labels` | `auto-sync,needs-manual-merge,needs-human-attention` | Comma-separated labels for fallback issue | +| `escalation_mentions` | `` | Optional mentions added to fallback issue body | +| `notification_webhook_url` | `` | Optional webhook URL for notifications (Slack-compatible incoming webhook) | +| `notification_on` | `human-intervention-required` | When to notify: `never`, `human-intervention-required`, or `all` | +| `fail_on_human_intervention` | `true` | If `true`, action exits non-zero when manual intervention is required. If `false`, action returns success and sets `result=human-intervention-required`. | + +## Token Input + +| Input | Required | Description | +|--------|----------|-------------| +| `token` | No | Auth token for push/PR operations. Falls back to `github.token` if not provided by caller workflow. | + +## Behavior + +### Success Case: No Conflicts +``` +push to develop + ↓ +workflow runs + ↓ +merge-base check: develop NOT in main ✓ + ↓ +attempt merge: success ✓ + ↓ +direct push to main: success ✓ + ↓ +✅ main updated, no PR created + +``` + +### Example 4: Optional Slack/Webhook notifications + +```yaml +jobs: + sync: + runs-on: comcast-ubuntu-latest + steps: + - uses: rdk-e/app-gateway-automation/actions/sync-branches@actions-v1 + with: + source_branch: develop + target_branch: main + token: ${{ github.token }} + notification_webhook_url: ${{ secrets.SYNC_ALERT_WEBHOOK_URL }} + notification_on: human-intervention-required +``` + +Notes: +- The webhook payload is JSON: `{ "text": "..." }`, which works with Slack Incoming Webhooks and many webhook relays. +- If `notification_webhook_url` is not set, no notification is sent. +- Notification delivery is best-effort and non-blocking: webhook failures emit a warning but do not fail the sync workflow. +### Fallback Case: Conflict or Protected Branch +``` +push to develop + ↓ +workflow runs + ↓ +merge-base check: develop NOT in main ✓ + ↓ +attempt merge: CONFLICT ✗ (or direct push fails due to protection) + ↓ +create branch: auto-sync/develop-to-main + ↓ +open/update PR with labels & reviewers + ↓ +open/update issue with compare link + run link + ↓ +⚠️ Human intervention required via PR/issue artifacts +``` + +The fallback issue contains: +- Snapshot source commit SHA used for the fallback branch +- Fallback branch name to merge from +- Compare, run, and one-click PR links +- A manual merge checklist for responders + +When GitHub policy blocks PR creation by Actions token, the workflow also writes +an explicit warning and one-click PR URL to the job summary for faster manual recovery. + +By default this path fails the action (`result=human-intervention-required` + non-zero exit). +Set `fail_on_human_intervention: false` in the consumer workflow if you want a non-blocking run while still creating escalation artifacts. + +### Idempotent Case: Already Synced +``` +push to develop (but develop already in main) + ↓ +workflow runs + ↓ +merge-base check: develop IS in main ✓ + ↓ +✅ Skip sync (nothing to do) +``` + +## Token Scope Reference + +For branch-protected targets, use a fine-grained PAT with: + +``` +Permissions: + - Contents: Read & write + - Pull requests: Read & write + +Repository access: + - All repositories (or specific repo) +``` + +[Create fine-grained PAT](https://github.com/settings/personal-access-tokens/new) + +## Troubleshooting + +**Q: The run says source or target branch does not exist. What should I do?** +- For `workflow_dispatch`, verify the exact `source_branch` and `target_branch` inputs. +- For push-triggered runs, the branch may have been renamed/deleted after the event fired. +- List remote branches with: + +```bash +gh api repos///branches --jq '.[].name' +``` + +**Q: How do I test this locally with act?** +- Run act against the workflow file under `.github/workflows/` (not `.github/actions/`): + +```bash +GITHUB_TOKEN="$(gh auth token)" act workflow_dispatch \ + -W .github/workflows/sync-develop-to-main.yml \ + -P comcast-ubuntu-latest=ghcr.io/catthehacker/ubuntu:act-latest \ + --input source_branch=develop \ + --input target_branch=main +``` + +- If the run fails with `gh: command not found` during fallback PR/issue steps, use an act runner image that includes the GitHub CLI, or treat local testing as partial and validate fallback artifact creation in GitHub-hosted runners. + +**Q: Workflow runs but push fails to protected branch** +- Ensure your token has `contents:write`, `pull-requests:write`, and `issues:write` +- If you are using the template, verify `RDKE_GITHUB_TOKEN` exists (or `SEMANTIC_RELEASE_TOKEN` as fallback) +- Confirm the workflow expression is `secrets.RDKE_GITHUB_TOKEN || secrets.SEMANTIC_RELEASE_TOKEN || github.token` + +**Q: PR opens but I don't want it** +- If it's just checking feature, use `workflow_dispatch` instead of automatic trigger +- Adjust branch protection rules if conflicts are expected + +**Q: Why did an issue open even when a fallback PR exists?** +- The action always upserts a fallback issue on human-intervention runs for consistent triage tracking. +- The issue body includes compare and run links plus current PR status. + +**Q: I need to sync multiple branch pairs** +- Create separate workflow files, each calling sync-branches with different `source_branch`/`target_branch` +- Or use a matrix job in your calling workflow + +## File Location + +This integration guide is stored in `actions/sync-branches/` for easy discovery. + +The composite action is at `actions/sync-branches/action.yml`. + +Versioned releases are published from `actions-vX.Y.Z` tags and include the action surface (`action.yml`, `consumer-template.yml`, `consumer-template-oss.yml`, `README.md`, `CHANGELOG.md`, and `scripts/`). + +--- + +**Integration Branch**: `feat/reusable-sync-automation` + +For the latest stable version, check the main branch or GitHub Releases. diff --git a/.github/actions/sync-branches/action.yml b/.github/actions/sync-branches/action.yml new file mode 100644 index 0000000..c4d544c --- /dev/null +++ b/.github/actions/sync-branches/action.yml @@ -0,0 +1,662 @@ +name: Sync Branches +description: > + Merge source branch into target branch and push directly. + On conflict or push failure, open/update fallback artifacts for human intervention. + Callers only need triggers, concurrency, and permissions. + +inputs: + source_branch: + description: Branch to sync from + required: false + default: develop + target_branch: + description: Branch to sync to + required: false + default: main + token: + description: Token for branch, PR, and issue operations. + required: false + default: "" + git_user_name: + description: Git committer name + required: false + default: github-actions[bot] + git_user_email: + description: Git committer email + required: false + default: github-actions[bot]@users.noreply.github.com + pr_branch_prefix: + description: Prefix for fallback PR branch + required: false + default: auto-sync + assign_reviewers: + description: Comma-separated reviewers for fallback PR + required: false + default: "" + fallback_pr_jira_key: + description: Optional Jira key override for fallback PR titles (for example RDKEMW-12345) + required: false + default: "" + pr_labels: + description: Comma-separated labels for fallback PR + required: false + default: auto-sync,needs-manual-merge + issue_labels: + description: Comma-separated labels for fallback issue + required: false + default: auto-sync,needs-manual-merge,needs-human-attention + escalation_mentions: + description: Optional mentions to include in fallback issue (for example @org/team) + required: false + default: "" + notification_webhook_url: + description: Optional webhook URL for notifications (for example Slack incoming webhook). + required: false + default: "" + notification_on: + description: "When to send webhook notifications: never | human-intervention-required | all" + required: false + default: "human-intervention-required" + fail_on_human_intervention: + description: Whether to fail the action when manual intervention is required. + required: false + default: "true" + +outputs: + result: + description: "Outcome: merged | skipped | human-intervention-required" + value: ${{ steps.outcome.outputs.result }} + pr_number: + description: "Fallback PR number (if any)" + value: ${{ steps.pr_fallback.outputs.pr_number }} + issue_number: + description: "Fallback issue number (if any)" + value: ${{ steps.issue_fallback.outputs.issue_number }} + +runs: + using: composite + steps: + - name: Resolve authentication token + id: token + shell: bash + env: + INPUT_TOKEN: ${{ inputs.token }} + run: | + if [[ -n "${INPUT_TOKEN}" ]]; then + echo "token=${INPUT_TOKEN}" >> "$GITHUB_OUTPUT" + echo "source=input:token" >> "$GITHUB_OUTPUT" + elif [[ -n "${GITHUB_TOKEN:-}" ]]; then + echo "token=${GITHUB_TOKEN}" >> "$GITHUB_OUTPUT" + echo "source=env:GITHUB_TOKEN" >> "$GITHUB_OUTPUT" + else + echo "token=${{ github.token }}" >> "$GITHUB_OUTPUT" + echo "source=implicit:github.token" >> "$GITHUB_OUTPUT" + fi + + - name: Resolve and validate branch inputs + id: branches + shell: bash + env: + INPUT_SOURCE_BRANCH: ${{ inputs.source_branch }} + INPUT_TARGET_BRANCH: ${{ inputs.target_branch }} + run: | + set -euo pipefail + + validate_branch() { + local branch="$1" + local label="$2" + + if [[ -z "$branch" ]]; then + echo "${label} branch must not be empty." >&2 + exit 1 + fi + + if ! git check-ref-format --branch "$branch" >/dev/null 2>&1; then + echo "Invalid ${label} branch '$branch'." >&2 + exit 1 + fi + } + + source_branch="$INPUT_SOURCE_BRANCH" + target_branch="$INPUT_TARGET_BRANCH" + + validate_branch "$source_branch" "source" + validate_branch "$target_branch" "target" + + echo "source_branch=${source_branch}" >> "$GITHUB_OUTPUT" + echo "target_branch=${target_branch}" >> "$GITHUB_OUTPUT" + + - name: Checkout full history + uses: actions/checkout@v4 + with: + token: ${{ steps.token.outputs.token }} + fetch-depth: 0 + + - name: Prepare sync workspace + id: workspace + shell: bash + env: + AUTH_TOKEN: ${{ steps.token.outputs.token }} + run: | + set -euo pipefail + workdir="$(mktemp -d)" + git clone . "$workdir" >/dev/null 2>&1 + git -C "$workdir" checkout --detach "$GITHUB_SHA" >/dev/null 2>&1 + git -C "$workdir" remote set-url origin "https://x-access-token:${AUTH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + echo "path=${workdir}" >> "$GITHUB_OUTPUT" + + - name: Configure git identity + shell: bash + env: + WORKDIR: ${{ steps.workspace.outputs.path }} + GIT_USER_NAME: ${{ inputs.git_user_name }} + GIT_USER_EMAIL: ${{ inputs.git_user_email }} + run: | + set -euo pipefail + git -C "$WORKDIR" config user.name "$GIT_USER_NAME" + git -C "$WORKDIR" config user.email "$GIT_USER_EMAIL" + + - name: Check if sync is needed + id: check + shell: bash + env: + WORKDIR: ${{ steps.workspace.outputs.path }} + SOURCE_BRANCH: ${{ steps.branches.outputs.source_branch }} + TARGET_BRANCH: ${{ steps.branches.outputs.target_branch }} + run: | + set -euo pipefail + + branch_exists_remote() { + local branch="$1" + git -C "$WORKDIR" ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1 + } + + if ! branch_exists_remote "$SOURCE_BRANCH"; then + echo "Sync aborted: source branch '${SOURCE_BRANCH}' does not exist on origin (${GITHUB_REPOSITORY})." >&2 + echo "Tips:" >&2 + echo " - If this was a workflow_dispatch run, verify the source_branch input value." >&2 + echo " - If this was a push-triggered run, the branch may have been renamed or deleted after the event fired." >&2 + echo " - Verify branch names with: gh api repos/${GITHUB_REPOSITORY}/branches --jq '.[].name'" >&2 + exit 1 + fi + + if ! branch_exists_remote "$TARGET_BRANCH"; then + echo "Sync aborted: target branch '${TARGET_BRANCH}' does not exist on origin (${GITHUB_REPOSITORY})." >&2 + echo "Tips:" >&2 + echo " - Confirm target_branch input for workflow_dispatch runs." >&2 + echo " - Ensure the destination branch exists (for example 'main')." >&2 + echo " - Verify branch names with: gh api repos/${GITHUB_REPOSITORY}/branches --jq '.[].name'" >&2 + exit 1 + fi + + git -C "$WORKDIR" fetch origin \ + "refs/heads/${SOURCE_BRANCH}:refs/remotes/origin/${SOURCE_BRANCH}" \ + "refs/heads/${TARGET_BRANCH}:refs/remotes/origin/${TARGET_BRANCH}" + + if git -C "$WORKDIR" merge-base --is-ancestor "origin/${SOURCE_BRANCH}" "origin/${TARGET_BRANCH}"; then + echo "already_synced=true" >> "$GITHUB_OUTPUT" + echo "${TARGET_BRANCH} already contains all commits from ${SOURCE_BRANCH}." + else + echo "already_synced=false" >> "$GITHUB_OUTPUT" + fi + + - name: Attempt merge + if: steps.check.outputs.already_synced == 'false' + id: merge + shell: bash + env: + WORKDIR: ${{ steps.workspace.outputs.path }} + SOURCE_BRANCH: ${{ steps.branches.outputs.source_branch }} + TARGET_BRANCH: ${{ steps.branches.outputs.target_branch }} + run: | + set -euo pipefail + git -C "$WORKDIR" checkout -B "$TARGET_BRANCH" "origin/${TARGET_BRANCH}" + if git -C "$WORKDIR" merge --no-edit "origin/${SOURCE_BRANCH}"; then + echo "conflict=false" >> "$GITHUB_OUTPUT" + else + echo "conflict=true" >> "$GITHUB_OUTPUT" + if git -C "$WORKDIR" rev-parse -q --verify MERGE_HEAD >/dev/null 2>&1; then + git -C "$WORKDIR" merge --abort || true + fi + fi + + - name: Push merged result + if: steps.check.outputs.already_synced == 'false' && steps.merge.outputs.conflict == 'false' + id: push + shell: bash + env: + WORKDIR: ${{ steps.workspace.outputs.path }} + SOURCE_BRANCH: ${{ steps.branches.outputs.source_branch }} + TARGET_BRANCH: ${{ steps.branches.outputs.target_branch }} + run: | + set -euo pipefail + if git -C "$WORKDIR" push origin "$TARGET_BRANCH"; then + echo "push_failed=false" >> "$GITHUB_OUTPUT" + echo "already_synced_after_push_reject=false" >> "$GITHUB_OUTPUT" + echo "Merged and pushed ${SOURCE_BRANCH} to ${TARGET_BRANCH}." + else + git -C "$WORKDIR" fetch origin "refs/heads/${TARGET_BRANCH}:refs/remotes/origin/${TARGET_BRANCH}" + if git -C "$WORKDIR" merge-base --is-ancestor "origin/${SOURCE_BRANCH}" "origin/${TARGET_BRANCH}"; then + echo "push_failed=false" >> "$GITHUB_OUTPUT" + echo "already_synced_after_push_reject=true" >> "$GITHUB_OUTPUT" + echo "Push was rejected because target advanced concurrently and already contains ${SOURCE_BRANCH}." + else + echo "push_failed=true" >> "$GITHUB_OUTPUT" + echo "already_synced_after_push_reject=false" >> "$GITHUB_OUTPUT" + echo "Direct push failed; preparing fallback branch and escalation artifacts." + fi + fi + + - name: Push fallback branch artifact + if: steps.check.outputs.already_synced == 'false' && (steps.merge.outputs.conflict == 'true' || steps.push.outputs.push_failed == 'true') + id: fallback_branch + shell: bash + env: + GH_TOKEN: ${{ steps.token.outputs.token }} + WORKDIR: ${{ steps.workspace.outputs.path }} + SOURCE_BRANCH: ${{ steps.branches.outputs.source_branch }} + TARGET_BRANCH: ${{ steps.branches.outputs.target_branch }} + PR_BRANCH_PREFIX: ${{ inputs.pr_branch_prefix }} + CONFLICT: ${{ steps.merge.outputs.conflict }} + run: | + set -euo pipefail + branch_prefix="${PR_BRANCH_PREFIX}/${SOURCE_BRANCH}-to-${TARGET_BRANCH}" + source_sha="$(git -C "$WORKDIR" rev-parse "origin/${SOURCE_BRANCH}")" + + reason="merge conflict" + if [[ "$CONFLICT" != "true" ]]; then + reason="direct push failed" + fi + + pr_list_json="$(gh pr list --base "$TARGET_BRANCH" --state open --limit 200 --json headRefName 2>/dev/null || echo '[]')" + existing_head="$(echo "$pr_list_json" | jq -r --arg prefix "$branch_prefix" 'map(select(.headRefName | startswith($prefix))) | .[0].headRefName // empty')" + + if [[ -n "$existing_head" ]]; then + echo "Reusing existing fallback branch ${existing_head}." + echo "fallback_branch=${existing_head}" >> "$GITHUB_OUTPUT" + echo "source_sha=${source_sha}" >> "$GITHUB_OUTPUT" + echo "reason=${reason}" >> "$GITHUB_OUTPUT" + exit 0 + fi + + pr_branch="$branch_prefix" + if git -C "$WORKDIR" ls-remote --exit-code --heads origin "$pr_branch" >/dev/null 2>&1; then + pr_branch="${pr_branch}-${GITHUB_RUN_ID}" + fi + + git -C "$WORKDIR" checkout -B "$pr_branch" "$source_sha" + git -C "$WORKDIR" push origin "$pr_branch" + + echo "fallback_branch=${pr_branch}" >> "$GITHUB_OUTPUT" + echo "source_sha=${source_sha}" >> "$GITHUB_OUTPUT" + echo "reason=${reason}" >> "$GITHUB_OUTPUT" + + - name: Ensure labels exist + if: steps.check.outputs.already_synced == 'false' && (steps.merge.outputs.conflict == 'true' || steps.push.outputs.push_failed == 'true') + shell: bash + env: + GH_TOKEN: ${{ steps.token.outputs.token }} + PR_LABELS: ${{ inputs.pr_labels }} + ISSUE_LABELS: ${{ inputs.issue_labels }} + run: | + set -euo pipefail + ensure_labels() { + local csv="$1" + IFS=',' read -ra labels <<< "$csv" + for label in "${labels[@]}"; do + label="$(echo "$label" | xargs)" + [[ -n "$label" ]] && gh label create "$label" --force || true + done + } + + ensure_labels "$PR_LABELS" + ensure_labels "$ISSUE_LABELS" + + - name: Open or update fallback PR + if: steps.check.outputs.already_synced == 'false' && (steps.merge.outputs.conflict == 'true' || steps.push.outputs.push_failed == 'true') + id: pr_fallback + shell: bash + env: + GH_TOKEN: ${{ steps.token.outputs.token }} + SOURCE_BRANCH: ${{ steps.branches.outputs.source_branch }} + TARGET_BRANCH: ${{ steps.branches.outputs.target_branch }} + REASON: ${{ steps.fallback_branch.outputs.reason }} + FALLBACK_BRANCH: ${{ steps.fallback_branch.outputs.fallback_branch }} + PR_BRANCH_PREFIX: ${{ inputs.pr_branch_prefix }} + WORKDIR: ${{ steps.workspace.outputs.path }} + CONFLICT: ${{ steps.merge.outputs.conflict }} + FALLBACK_PR_JIRA_KEY: ${{ inputs.fallback_pr_jira_key }} + PR_LABELS: ${{ inputs.pr_labels }} + ASSIGN_REVIEWERS: ${{ inputs.assign_reviewers }} + run: | + set -euo pipefail + source_branch="$SOURCE_BRANCH" + target_branch="$TARGET_BRANCH" + reason="$REASON" + fallback_branch="$FALLBACK_BRANCH" + branch_prefix="${PR_BRANCH_PREFIX}/${source_branch}-to-${target_branch}" + workdir="$WORKDIR" + + extract_jira_key() { + local text="$1" + local key="" + key="$(echo "$text" | grep -oE '[A-Z][A-Z0-9]+-[0-9]+' | head -n1 || true)" + echo "$key" + } + + jira_key="" + jira_key_override="$FALLBACK_PR_JIRA_KEY" + if [[ -n "$jira_key_override" ]]; then + jira_key="$(extract_jira_key "$jira_key_override")" + if [[ -z "$jira_key" ]]; then + echo "fallback_pr_jira_key must include a Jira key like ABC-1234." >&2 + exit 1 + fi + fi + + if [[ -z "$jira_key" ]]; then + jira_key="$(extract_jira_key "$source_branch")" + fi + + if [[ -z "$jira_key" ]]; then + source_head_subject="$(git -C "$workdir" log -1 --pretty=%s "origin/${source_branch}" 2>/dev/null || true)" + jira_key="$(extract_jira_key "$source_head_subject")" + fi + + if [[ -z "$jira_key" ]]; then + jira_key="RDKDEV-0000" + echo "No Jira key detected from override, source branch, or source HEAD subject; using ${jira_key}." >&2 + fi + + pr_list_json="$(gh pr list --base "$target_branch" --state open --json number,headRefName 2>/dev/null || echo '[]')" + existing_pr="$(echo "$pr_list_json" | jq -r --arg prefix "$branch_prefix" 'map(select(.headRefName | startswith($prefix))) | .[0].number // empty')" + existing_head="$(echo "$pr_list_json" | jq -r --arg prefix "$branch_prefix" 'map(select(.headRefName | startswith($prefix))) | .[0].headRefName // empty')" + + pr_title="${jira_key}: auto-sync ${source_branch} -> ${target_branch} (${reason})" + compare_branch="$fallback_branch" + if [[ -z "$compare_branch" && -n "$existing_head" ]]; then + compare_branch="$existing_head" + fi + compare_url="https://github.com/${GITHUB_REPOSITORY}/compare/${target_branch}...${compare_branch}?expand=1" + printf -v pr_body '%s\n\nReason: %s\nFallback branch: %s\nCompare: %s\nRun: %s' \ + "Automatic sync of ${source_branch} to ${target_branch} requires human intervention." \ + "$reason" \ + "${compare_branch}" \ + "$compare_url" \ + "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + + if [[ -n "$existing_pr" ]]; then + if [[ "$CONFLICT" != "true" && -n "$existing_head" ]]; then + git -C "$workdir" fetch origin "refs/heads/${existing_head}:refs/remotes/origin/${existing_head}" + git -C "$workdir" checkout -B "$existing_head" "origin/${source_branch}" + git -C "$workdir" push --force-with-lease origin "$existing_head" + echo "Updated fallback PR branch ${existing_head} to latest ${source_branch}." + fi + if ! gh pr edit "$existing_pr" --title "$pr_title" --body "$pr_body" >/dev/null 2>&1; then + echo "Warning: failed to update title/body for fallback PR #${existing_pr}." >&2 + fi + echo "pr_number=${existing_pr}" >> "$GITHUB_OUTPUT" + echo "pr_permission_denied=false" >> "$GITHUB_OUTPUT" + echo "pr_error_message=" >> "$GITHUB_OUTPUT" + exit 0 + fi + + pr_create_args=( + pr create + --base "$target_branch" + --head "$fallback_branch" + --title "$pr_title" + --body "$pr_body" + ) + + IFS=',' read -ra labels <<< "$PR_LABELS" + for label in "${labels[@]}"; do + label="$(echo "$label" | xargs)" + [[ -n "$label" ]] && pr_create_args+=(--label "$label") + done + + IFS=',' read -ra reviewers <<< "$ASSIGN_REVIEWERS" + for reviewer in "${reviewers[@]}"; do + reviewer="$(echo "$reviewer" | xargs)" + [[ -n "$reviewer" ]] && pr_create_args+=(--reviewer "$reviewer") + done + + pr_create_output_file="$(mktemp)" + set +e + gh "${pr_create_args[@]}" >"$pr_create_output_file" 2>&1 + status=$? + set -e + + if [[ $status -eq 0 ]]; then + pr_url="$(cat "$pr_create_output_file")" + pr_number="$(gh pr view "$pr_url" --json number --jq .number 2>/dev/null || true)" + if [[ -z "$pr_number" ]]; then + pr_number="$(gh pr list --base "$target_branch" --head "$fallback_branch" --state open --json number --jq '.[0].number // empty' 2>/dev/null || true)" + fi + if [[ -z "$pr_number" ]]; then + echo "Created PR but failed to resolve its number." >&2 + cat "$pr_create_output_file" >&2 + echo "pr_number=" >> "$GITHUB_OUTPUT" + echo "pr_permission_denied=false" >> "$GITHUB_OUTPUT" + echo "pr_error_message=Created PR but could not resolve PR number." >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "pr_number=${pr_number}" >> "$GITHUB_OUTPUT" + echo "pr_permission_denied=false" >> "$GITHUB_OUTPUT" + echo "pr_error_message=" >> "$GITHUB_OUTPUT" + exit 0 + fi + + create_err="$(cat "$pr_create_output_file")" + if echo "$create_err" | grep -Eq 'createPullRequest|not permitted to create or approve pull requests'; then + manual_pr_url="https://github.com/${GITHUB_REPOSITORY}/compare/${target_branch}...${fallback_branch}?expand=1&quick_pull=1" + echo "::warning::GitHub Actions token cannot create PRs in this repository policy. Open PR manually: ${manual_pr_url}" + { + echo "### PR Creation Blocked By Policy" + echo "- Reason: GitHub Actions token is not permitted to create/approve pull requests" + echo "- One-click PR: ${manual_pr_url}" + echo "- Fallback branch: ${fallback_branch}" + } >> "$GITHUB_STEP_SUMMARY" + echo "pr_number=" >> "$GITHUB_OUTPUT" + echo "pr_permission_denied=true" >> "$GITHUB_OUTPUT" + echo "pr_error_message=$(echo "$create_err" | tr '\n' ' ')" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "pr_number=" >> "$GITHUB_OUTPUT" + echo "pr_permission_denied=false" >> "$GITHUB_OUTPUT" + echo "pr_error_message=$(echo "$create_err" | tr '\n' ' ')" >> "$GITHUB_OUTPUT" + exit 0 + + - name: Open or update fallback issue + if: steps.check.outputs.already_synced == 'false' && (steps.merge.outputs.conflict == 'true' || steps.push.outputs.push_failed == 'true') + id: issue_fallback + shell: bash + env: + GH_TOKEN: ${{ steps.token.outputs.token }} + SOURCE_BRANCH: ${{ steps.branches.outputs.source_branch }} + TARGET_BRANCH: ${{ steps.branches.outputs.target_branch }} + SOURCE_SHA: ${{ steps.fallback_branch.outputs.source_sha }} + REASON: ${{ steps.fallback_branch.outputs.reason }} + FALLBACK_BRANCH: ${{ steps.fallback_branch.outputs.fallback_branch }} + PR_NUMBER: ${{ steps.pr_fallback.outputs.pr_number }} + PR_PERMISSION_DENIED: ${{ steps.pr_fallback.outputs.pr_permission_denied }} + PR_ERROR_MESSAGE: ${{ steps.pr_fallback.outputs.pr_error_message }} + ESCALATION_MENTIONS: ${{ inputs.escalation_mentions }} + ISSUE_LABELS: ${{ inputs.issue_labels }} + run: | + set -euo pipefail + source_branch="$SOURCE_BRANCH" + target_branch="$TARGET_BRANCH" + source_sha="$SOURCE_SHA" + reason="$REASON" + fallback_branch="$FALLBACK_BRANCH" + compare_url="https://github.com/${GITHUB_REPOSITORY}/compare/${target_branch}...${fallback_branch}?expand=1" + pr_create_url="https://github.com/${GITHUB_REPOSITORY}/compare/${target_branch}...${fallback_branch}?expand=1&quick_pull=1" + run_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + + title="chore: auto-sync ${source_branch} -> ${target_branch} needs manual merge" + + pr_note="Fallback PR was updated or created successfully." + if [[ -n "$PR_NUMBER" ]]; then + pr_note="Fallback PR #${PR_NUMBER} is available for manual merge." + elif [[ "$PR_PERMISSION_DENIED" == "true" ]]; then + pr_note="Fallback PR creation was denied by token policy." + elif [[ -n "$PR_ERROR_MESSAGE" ]]; then + pr_note="Fallback PR creation encountered an error: ${PR_ERROR_MESSAGE}" + fi + + printf -v body '%s\n\nReason: %s\nSource branch: %s\nSource commit: %s\nFallback branch: %s\nCompare: %s\nCreate PR: %s\nRun: %s\n\nPR status: %s\n\nManual merge checklist:\n- Checkout `%s` and merge `%s` into it\n- Resolve conflicts\n- Open PR with one click (base `%s`): %s\n- Merge PR after checks pass\n- Close this issue' \ + "Automatic sync from ${source_branch} to ${target_branch} requires manual intervention." \ + "$reason" \ + "$source_branch" \ + "$source_sha" \ + "$fallback_branch" \ + "$compare_url" \ + "$pr_create_url" \ + "$run_url" \ + "$pr_note" \ + "$fallback_branch" \ + "$source_branch" \ + "$target_branch" \ + "$pr_create_url" + if [[ -n "$ESCALATION_MENTIONS" ]]; then + printf -v body '%s\n\nAttention: %s' "$body" "$ESCALATION_MENTIONS" + fi + + issue_list_json="$(gh issue list --state open --search "\"${title}\" in:title" --json number,title 2>/dev/null || echo '[]')" + existing_issue="$(echo "$issue_list_json" | jq -r --arg title "$title" 'map(select(.title == $title)) | .[0].number // empty')" + + if [[ -n "$existing_issue" ]]; then + gh issue edit "$existing_issue" --title "$title" --body "$body" + IFS=',' read -ra labels <<< "$ISSUE_LABELS" + for label in "${labels[@]}"; do + label="$(echo "$label" | xargs)" + [[ -n "$label" ]] && gh issue edit "$existing_issue" --add-label "$label" >/dev/null + done + echo "issue_number=${existing_issue}" >> "$GITHUB_OUTPUT" + exit 0 + fi + + issue_args=(issue create --title "$title" --body "$body") + IFS=',' read -ra labels <<< "$ISSUE_LABELS" + for label in "${labels[@]}"; do + label="$(echo "$label" | xargs)" + [[ -n "$label" ]] && issue_args+=(--label "$label") + done + + issue_url="$(gh "${issue_args[@]}")" + issue_number="$(gh issue view "$issue_url" --json number --jq .number 2>/dev/null || true)" + if [[ -z "$issue_number" ]]; then + issue_number="$(gh issue list --state open --search "\"${title}\" in:title" --json number,title | jq -r --arg title "$title" 'map(select(.title == $title)) | .[0].number // empty' 2>/dev/null || true)" + fi + if [[ -z "$issue_number" ]]; then + echo "Created issue but failed to resolve its number." >&2 + exit 1 + fi + echo "issue_number=${issue_number}" >> "$GITHUB_OUTPUT" + + - name: Cleanup sync workspace + if: always() + shell: bash + run: | + workdir="${{ steps.workspace.outputs.path }}" + [[ -n "$workdir" ]] && rm -rf "$workdir" + + - name: Determine outcome + id: outcome + if: always() + shell: bash + env: + ALREADY_SYNCED: ${{ steps.check.outputs.already_synced }} + MERGE_CONFLICT: ${{ steps.merge.outputs.conflict }} + PUSH_FAILED: ${{ steps.push.outputs.push_failed }} + ALREADY_SYNCED_AFTER_PUSH_REJECT: ${{ steps.push.outputs.already_synced_after_push_reject }} + FAIL_ON_HUMAN_INTERVENTION: ${{ inputs.fail_on_human_intervention }} + run: | + if [[ -z "${ALREADY_SYNCED:-}" ]]; then + echo "result=failed" >> "$GITHUB_OUTPUT" + echo "Unable to determine sync result because a prior step did not publish required outputs." >&2 + exit 1 + fi + + if [[ "$ALREADY_SYNCED" == "true" ]]; then + echo "result=skipped" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if [[ -z "${MERGE_CONFLICT:-}" || -z "${PUSH_FAILED:-}" ]]; then + echo "result=failed" >> "$GITHUB_OUTPUT" + echo "Unable to determine merge/push outcome from prior steps." >&2 + exit 1 + fi + + if [[ "$MERGE_CONFLICT" == "false" && "$PUSH_FAILED" == "false" ]]; then + if [[ "$ALREADY_SYNCED_AFTER_PUSH_REJECT" == "true" ]]; then + echo "result=skipped" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "result=merged" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "result=human-intervention-required" >> "$GITHUB_OUTPUT" + normalized_fail="$(echo "${FAIL_ON_HUMAN_INTERVENTION}" | tr '[:upper:]' '[:lower:]')" + if [[ "$normalized_fail" == "true" || "$normalized_fail" == "1" || "$normalized_fail" == "yes" ]]; then + exit 1 + fi + + echo "Manual intervention required; continuing because fail_on_human_intervention is disabled." + exit 0 + + - name: Send optional webhook notification + if: always() && inputs.notification_webhook_url != '' + continue-on-error: true + shell: bash + env: + WEBHOOK_URL: ${{ inputs.notification_webhook_url }} + NOTIFICATION_ON: ${{ inputs.notification_on }} + RESULT: ${{ steps.outcome.outputs.result }} + SOURCE_BRANCH: ${{ steps.branches.outputs.source_branch }} + TARGET_BRANCH: ${{ steps.branches.outputs.target_branch }} + PR_NUMBER: ${{ steps.pr_fallback.outputs.pr_number }} + ISSUE_NUMBER: ${{ steps.issue_fallback.outputs.issue_number }} + run: | + set -euo pipefail + + result="${RESULT:-unknown}" + notify_on="$(echo "${NOTIFICATION_ON}" | tr '[:upper:]' '[:lower:]')" + + case "$notify_on" in + never) + exit 0 + ;; + human-intervention-required) + [[ "$result" == "human-intervention-required" ]] || exit 0 + ;; + all) + ;; + *) + echo "::warning::Invalid notification_on value '${NOTIFICATION_ON}'. Expected: never | human-intervention-required | all. Skipping notification." + exit 0 + ;; + esac + + run_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + message="[${GITHUB_REPOSITORY}] sync ${SOURCE_BRANCH} -> ${TARGET_BRANCH}: ${result}. Run: ${run_url}" + + if [[ -n "${PR_NUMBER:-}" ]]; then + message+=" PR: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pull/${PR_NUMBER}" + fi + if [[ -n "${ISSUE_NUMBER:-}" ]]; then + message+=" Issue: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/issues/${ISSUE_NUMBER}" + fi + + payload="$(jq -n --arg text "$message" '{text: $text}')" + if ! curl -fsS --max-time 10 --retry 1 -X POST -H 'Content-Type: application/json' --data "$payload" "$WEBHOOK_URL" >/dev/null; then + echo "::warning::Notification webhook delivery failed; continuing without failing the workflow." + exit 0 + fi + + echo "Sent webhook notification for result '${result}'." diff --git a/.github/actions/sync-branches/consumer-template.yml b/.github/actions/sync-branches/consumer-template.yml new file mode 100644 index 0000000..2f0483f --- /dev/null +++ b/.github/actions/sync-branches/consumer-template.yml @@ -0,0 +1,46 @@ +# Sync develop -> main -- consumer template +# +# Copy this file to .github/workflows/sync-develop-to-main.yml in any consumer repo. +# Do not place this file under .github/actions/; GitHub only loads workflow YAML from .github/workflows/. +# The sync logic lives in app-gateway-automation/actions/sync-branches. +# +# Optional secrets (first match wins): +# - RDKE_GITHUB_TOKEN +# - SEMANTIC_RELEASE_TOKEN +# Falls back to github.token if neither secret exists. + +name: Sync develop to main + +on: + push: + branches: [develop] + workflow_dispatch: + inputs: + source_branch: + description: "Source branch" + required: false + default: "develop" + target_branch: + description: "Target branch" + required: false + default: "main" + +concurrency: + group: sync-${{ github.repository }}-${{ github.event_name == 'workflow_dispatch' && format('{0}-to-{1}', github.event.inputs.source_branch || 'develop', github.event.inputs.target_branch || 'main') || format('{0}-to-{1}', github.ref_name, 'main') }} + cancel-in-progress: ${{ github.event_name != 'workflow_dispatch' }} + +permissions: + contents: write + pull-requests: write + issues: write + +jobs: + sync: + name: Merge ${{ github.event.inputs.source_branch || 'develop' }} → ${{ github.event.inputs.target_branch || 'main' }} + runs-on: comcast-ubuntu-latest # use org runner label; change if repo uses a different label + steps: + - uses: rdk-e/app-gateway-automation/actions/sync-branches@actions-v1 + with: + source_branch: ${{ github.event.inputs.source_branch || 'develop' }} + target_branch: ${{ github.event.inputs.target_branch || 'main' }} + token: ${{ secrets.RDKE_GITHUB_TOKEN || secrets.SEMANTIC_RELEASE_TOKEN || github.token }} diff --git a/.github/actions/sync-branches/scripts/move-major-tag.sh b/.github/actions/sync-branches/scripts/move-major-tag.sh new file mode 100755 index 0000000..3112512 --- /dev/null +++ b/.github/actions/sync-branches/scripts/move-major-tag.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Move floating major tag to point to the new release +# Usage: move-major-tag.sh --version +# Example: move-major-tag.sh --version 1.2.3 +# → creates/updates actions-v1 to point to actions-v1.2.3 + +VERSION="" +while [[ $# -gt 0 ]]; do + case "$1" in + --version) + VERSION="$2" + shift 2 + ;; + *) + shift + ;; + esac +done + +if [ -z "$VERSION" ]; then + echo "ERROR: Usage: move-major-tag.sh --version " + exit 1 +fi + +# Skip prerelease versions (e.g. 1.2.3-rc.1) — don't move the stable major tag +if [[ "$VERSION" == *-* ]]; then + echo "Skipping major tag update for prerelease version: $VERSION" + exit 0 +fi + +# Extract major version from semver +MAJOR=$(echo "$VERSION" | cut -d. -f1) + +# Configure git if needed +if ! git config user.name 2>/dev/null; then + git config user.name "github-actions[bot]" +fi +if ! git config user.email 2>/dev/null; then + git config user.email "github-actions[bot]@users.noreply.github.com" +fi + +# Move floating major tag +MAJOR_TAG="actions-v${MAJOR}" +VERSION_TAG="actions-v${VERSION}" +TARGET_COMMIT=$(git rev-parse "${VERSION_TAG}^{commit}" 2>/dev/null || true) + +if [ -z "$TARGET_COMMIT" ]; then + echo "ERROR: Failed to resolve commit for $VERSION_TAG" + exit 1 +fi + +git tag -f "$MAJOR_TAG" "$TARGET_COMMIT" || { + echo "ERROR: Failed to tag $MAJOR_TAG -> $TARGET_COMMIT" + exit 1 +} + +git push origin "refs/tags/$MAJOR_TAG" --force || { + echo "ERROR: Failed to push $MAJOR_TAG" + exit 1 +} + +echo "Moved $MAJOR_TAG → $VERSION_TAG ($TARGET_COMMIT)" diff --git a/.github/automation-install.lock.json b/.github/automation-install.lock.json new file mode 100644 index 0000000..9e8b56b --- /dev/null +++ b/.github/automation-install.lock.json @@ -0,0 +1,25 @@ +{ + "schema_version": 1, + "installer_version": "1.0.0", + "installed_at": "2026-06-29T23:05:35Z", + "modules": [ + { + "module": "sync-branches", + "version": "1.0.99-test", + "source": "sync-branches-1.0.99-test.tar.gz", + "managed_files": [ + ".github/actions/sync-branches/action.yml", + ".github/actions/sync-branches/consumer-template.yml", + ".github/actions/sync-branches/README.md", + ".github/actions/sync-branches/scripts/move-major-tag.sh", + ".github/workflows/sync-develop-to-main.yml" + ], + "template_values": { + "source_branch": "develop", + "target_branch": "main", + "runner": "comcast-ubuntu-latest", + "secret_name": "SEMANTIC_RELEASE_TOKEN" + } + } + ] +} diff --git a/.github/workflows/sync-develop-to-main.yml b/.github/workflows/sync-develop-to-main.yml new file mode 100644 index 0000000..8d27743 --- /dev/null +++ b/.github/workflows/sync-develop-to-main.yml @@ -0,0 +1,44 @@ +# Sync develop -> main -- consumer template +# +# The sync logic lives in app-gateway-automation/actions/sync-branches. +# +# Optional secret: SEMANTIC_RELEASE_TOKEN (contents:write + pull-requests:write + issues:write) +# Used for sync operations when present; falls back to github.token if absent. + +name: Sync develop to main + +on: + push: + branches: [develop] + workflow_dispatch: + inputs: + source_branch: + description: "Source branch" + required: false + default: "develop" + target_branch: + description: "Target branch" + required: false + default: "main" + +concurrency: + group: sync-${{ github.repository }}-${{ github.event_name == 'workflow_dispatch' && format('{0}-to-{1}', github.event.inputs.source_branch || 'develop', github.event.inputs.target_branch || 'main') || format('{0}-to-{1}', github.ref_name, 'main') }} + cancel-in-progress: ${{ github.event_name != 'workflow_dispatch' }} + +permissions: + contents: write + pull-requests: write + issues: write + +jobs: + sync: + name: Merge ${{ github.event.inputs.source_branch || 'develop' }} → ${{ github.event.inputs.target_branch || 'main' }} + runs-on: comcast-ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - uses: ./.github/actions/sync-branches + with: + source_branch: ${{ github.event.inputs.source_branch || 'develop' }} + target_branch: ${{ github.event.inputs.target_branch || 'main' }} + token: ${{ secrets.SEMANTIC_RELEASE_TOKEN || github.token }}