diff --git a/.github/workflows/auto-merge-release-please.yml b/.github/workflows/auto-merge-release-please.yml deleted file mode 100644 index b999ba6..0000000 --- a/.github/workflows/auto-merge-release-please.yml +++ /dev/null @@ -1,193 +0,0 @@ -name: Auto-merge Release Please PRs - -on: - pull_request_target: - branches: [production] - types: [opened, reopened, synchronize, ready_for_review] - workflow_dispatch: - inputs: - pull_request_number: - description: "Optional PR number to process manually" - required: false - type: string - -concurrency: - group: auto-merge-release-please-${{ github.event.pull_request.number || github.run_id }} - cancel-in-progress: true - -permissions: - contents: read - pull-requests: write - issues: write - -jobs: - auto_merge_release_please: - name: Enable rebase auto-merge for Release Please PR - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Enable rebase auto-merge - uses: actions/github-script@v7 - env: - RELEASE_PR_HEAD_PREFIX: release-please--branches--production - CONFLICT_MARKER: "" - with: - github-token: ${{ secrets.RELEASE_PLEASE_TOKEN }} - script: | - const owner = context.repo.owner; - const repo = context.repo.repo; - const headPrefix = process.env.RELEASE_PR_HEAD_PREFIX; - const conflictMarker = process.env.CONFLICT_MARKER; - - let payloadPr = context.payload.pull_request; - - if (!payloadPr) { - const manualNumberRaw = context.payload.inputs && context.payload.inputs.pull_request_number - ? context.payload.inputs.pull_request_number - : ""; - - if (!manualNumberRaw) { - core.info("No pull_request payload found and no manual PR number provided. Nothing to do."); - return; - } - - const manualNumber = Number(manualNumberRaw); - if (!Number.isInteger(manualNumber) || manualNumber <= 0) { - core.setFailed(`Invalid pull_request_number input: "${manualNumberRaw}"`); - return; - } - - const { data: fetchedPr } = await github.rest.pulls.get({ - owner, - repo, - pull_number: manualNumber, - }); - payloadPr = fetchedPr; - } - - if ((payloadPr.base && payloadPr.base.ref) !== "production") { - core.info(`Skipping PR #${payloadPr.number}: base is not production.`); - return; - } - - if (!payloadPr.head || !payloadPr.head.ref || !payloadPr.head.ref.startsWith(headPrefix)) { - core.info(`Skipping PR #${payloadPr.number}: not a Release Please branch.`); - return; - } - - if (payloadPr.draft) { - core.info(`Skipping PR #${payloadPr.number}: PR is draft.`); - return; - } - - const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); - - const upsertConflictComment = async (issueNumber, body) => { - const comments = await github.paginate(github.rest.issues.listComments, { - owner, - repo, - issue_number: issueNumber, - per_page: 100, - }); - - const existing = comments.find((comment) => comment.body && comment.body.includes(conflictMarker)); - - if (!body) { - if (existing) { - await github.rest.issues.deleteComment({ - owner, - repo, - comment_id: existing.id, - }); - core.info(`Removed obsolete conflict comment from PR #${issueNumber}.`); - } - return; - } - - if (existing) { - await github.rest.issues.updateComment({ - owner, - repo, - comment_id: existing.id, - body, - }); - core.info(`Updated conflict comment on PR #${issueNumber}.`); - return; - } - - await github.rest.issues.createComment({ - owner, - repo, - issue_number: issueNumber, - body, - }); - core.info(`Created conflict comment on PR #${issueNumber}.`); - }; - - const waitForMergeable = async (pullNumber) => { - for (let attempt = 1; attempt <= 8; attempt += 1) { - const { data: pr } = await github.rest.pulls.get({ - owner, - repo, - pull_number: pullNumber, - }); - - if (pr.mergeable !== null) { - return pr; - } - - core.info(`mergeable is null for PR #${pullNumber}; retry ${attempt}/8...`); - await sleep(2000); - } - - const { data: pr } = await github.rest.pulls.get({ - owner, - repo, - pull_number: pullNumber, - }); - return pr; - }; - - let pr = await waitForMergeable(payloadPr.number); - - if (pr.mergeable === false) { - const body = [ - conflictMarker, - "Release Please PR auto-merge is blocked because this PR is not currently mergeable.", - "", - "Please resolve the merge conflict. Auto-merge will be retried when the PR updates.", - ].join("\n"); - await upsertConflictComment(pr.number, body); - core.warning(`Release Please PR #${pr.number} is not mergeable right now.`); - return; - } - - await upsertConflictComment(pr.number, ""); - - try { - await github.graphql( - ` - mutation EnableAutoMerge($pullRequestId: ID!) { - enablePullRequestAutoMerge( - input: { pullRequestId: $pullRequestId, mergeMethod: REBASE } - ) { - pullRequest { - number - } - } - } - `, - { - pullRequestId: pr.node_id, - }, - ); - core.info(`Enabled REBASE auto-merge for Release Please PR #${pr.number}.`); - } catch (error) { - const message = String(error && error.message ? error.message : error); - if (message.toLowerCase().includes("already has auto-merge enabled")) { - core.info(`REBASE auto-merge already enabled for Release Please PR #${pr.number}.`); - return; - } - throw error; - } diff --git a/.github/workflows/backsync-production-to-staging.yml b/.github/workflows/backsync-production-to-staging.yml deleted file mode 100644 index eb2e1c3..0000000 --- a/.github/workflows/backsync-production-to-staging.yml +++ /dev/null @@ -1,92 +0,0 @@ -name: Sync production -> staging - -on: - push: - branches: [production] - workflow_dispatch: - -concurrency: - group: sync-production-to-staging - cancel-in-progress: true - -permissions: - contents: write - -jobs: - sync: - name: Rebase-sync staging onto production - runs-on: ubuntu-latest - timeout-minutes: 20 - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.RELEASE_PLEASE_TOKEN }} - - - name: Rebase-sync target branch - env: - SOURCE_BRANCH: production - TARGET_BRANCH: staging - REPO: ${{ github.repository }} - TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }} - run: | - set -euo pipefail - - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git remote set-url origin "https://x-access-token:${TOKEN}@github.com/${REPO}.git" - - git fetch origin "${SOURCE_BRANCH}" "${TARGET_BRANCH}" - - SOURCE_REF="origin/${SOURCE_BRANCH}" - TARGET_REF="origin/${TARGET_BRANCH}" - - SOURCE_SHA="$(git rev-parse "${SOURCE_REF}")" - TARGET_SHA="$(git rev-parse "${TARGET_REF}")" - - echo "source=${SOURCE_REF} (${SOURCE_SHA})" - echo "target=${TARGET_REF} (${TARGET_SHA})" - - if [[ "${SOURCE_SHA}" == "${TARGET_SHA}" ]]; then - echo "Branches are already identical." - exit 0 - fi - - if git merge-base --is-ancestor "${SOURCE_REF}" "${TARGET_REF}"; then - echo "Target already contains source. No sync needed." - exit 0 - fi - - if git merge-base --is-ancestor "${TARGET_REF}" "${SOURCE_REF}"; then - echo "Fast-forwarding target to source." - git checkout -B "${TARGET_BRANCH}" "${SOURCE_REF}" - else - TARGET_PATCH_UNIQUE_COMMITS="$(git rev-list --reverse --right-only --cherry-pick --no-merges "${SOURCE_REF}...${TARGET_REF}")" - - if [[ -z "${TARGET_PATCH_UNIQUE_COMMITS}" ]]; then - echo "Branches diverged only by patch-equivalent commits. Resetting target to source." - git checkout -B "${TARGET_BRANCH}" "${SOURCE_REF}" - else - echo "Branches diverged with target-only patch-unique commits." - echo "${TARGET_PATCH_UNIQUE_COMMITS}" | sed 's/^/target-only-commit: /' - echo "Rebuilding target on top of source via cherry-pick." - git checkout -B "${TARGET_BRANCH}" "${SOURCE_REF}" - while IFS= read -r commit_sha; do - [[ -z "${commit_sha}" ]] && continue - git cherry-pick "${commit_sha}" - done <<< "${TARGET_PATCH_UNIQUE_COMMITS}" - fi - fi - - NEW_TARGET_SHA="$(git rev-parse "${TARGET_BRANCH}")" - echo "new-target=${NEW_TARGET_SHA}" - - if [[ "${NEW_TARGET_SHA}" == "${TARGET_SHA}" ]]; then - echo "No branch update required after rebase." - exit 0 - fi - - git push --force-with-lease="${TARGET_BRANCH}:${TARGET_SHA}" origin "${TARGET_BRANCH}:${TARGET_BRANCH}" - echo "Updated ${TARGET_BRANCH} to ${NEW_TARGET_SHA}." diff --git a/.github/workflows/backsync-staging-to-development.yml b/.github/workflows/backsync-staging-to-development.yml deleted file mode 100644 index 445438d..0000000 --- a/.github/workflows/backsync-staging-to-development.yml +++ /dev/null @@ -1,92 +0,0 @@ -name: Sync staging -> development - -on: - push: - branches: [staging] - workflow_dispatch: - -concurrency: - group: sync-staging-to-development - cancel-in-progress: true - -permissions: - contents: write - -jobs: - sync: - name: Rebase-sync development onto staging - runs-on: ubuntu-latest - timeout-minutes: 20 - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.RELEASE_PLEASE_TOKEN }} - - - name: Rebase-sync target branch - env: - SOURCE_BRANCH: staging - TARGET_BRANCH: development - REPO: ${{ github.repository }} - TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }} - run: | - set -euo pipefail - - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git remote set-url origin "https://x-access-token:${TOKEN}@github.com/${REPO}.git" - - git fetch origin "${SOURCE_BRANCH}" "${TARGET_BRANCH}" - - SOURCE_REF="origin/${SOURCE_BRANCH}" - TARGET_REF="origin/${TARGET_BRANCH}" - - SOURCE_SHA="$(git rev-parse "${SOURCE_REF}")" - TARGET_SHA="$(git rev-parse "${TARGET_REF}")" - - echo "source=${SOURCE_REF} (${SOURCE_SHA})" - echo "target=${TARGET_REF} (${TARGET_SHA})" - - if [[ "${SOURCE_SHA}" == "${TARGET_SHA}" ]]; then - echo "Branches are already identical." - exit 0 - fi - - if git merge-base --is-ancestor "${SOURCE_REF}" "${TARGET_REF}"; then - echo "Target already contains source. No sync needed." - exit 0 - fi - - if git merge-base --is-ancestor "${TARGET_REF}" "${SOURCE_REF}"; then - echo "Fast-forwarding target to source." - git checkout -B "${TARGET_BRANCH}" "${SOURCE_REF}" - else - TARGET_PATCH_UNIQUE_COMMITS="$(git rev-list --reverse --right-only --cherry-pick --no-merges "${SOURCE_REF}...${TARGET_REF}")" - - if [[ -z "${TARGET_PATCH_UNIQUE_COMMITS}" ]]; then - echo "Branches diverged only by patch-equivalent commits. Resetting target to source." - git checkout -B "${TARGET_BRANCH}" "${SOURCE_REF}" - else - echo "Branches diverged with target-only patch-unique commits." - echo "${TARGET_PATCH_UNIQUE_COMMITS}" | sed 's/^/target-only-commit: /' - echo "Rebuilding target on top of source via cherry-pick." - git checkout -B "${TARGET_BRANCH}" "${SOURCE_REF}" - while IFS= read -r commit_sha; do - [[ -z "${commit_sha}" ]] && continue - git cherry-pick "${commit_sha}" - done <<< "${TARGET_PATCH_UNIQUE_COMMITS}" - fi - fi - - NEW_TARGET_SHA="$(git rev-parse "${TARGET_BRANCH}")" - echo "new-target=${NEW_TARGET_SHA}" - - if [[ "${NEW_TARGET_SHA}" == "${TARGET_SHA}" ]]; then - echo "No branch update required after rebase." - exit 0 - fi - - git push --force-with-lease="${TARGET_BRANCH}:${TARGET_SHA}" origin "${TARGET_BRANCH}:${TARGET_BRANCH}" - echo "Updated ${TARGET_BRANCH} to ${NEW_TARGET_SHA}." diff --git a/README.md b/README.md index 43803ac..43aeedf 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,66 @@ To build a new .ipa file for iOS switch to the `production` branch and run the f flutter build ipa --flavor production --dart-define-from-file=.env.production --release --export-method app-store --build-name="$(grep -E '^version:' pubspec.yaml | awk '{print $2}' | cut -d+ -f1)" --build-number="$(date -u +%s)" ``` +### Working with git + +1. Start work (feature branch from `development`) + +```sh +git fetch origin --prune +git switch development +git pull --ff-only origin development +git switch -c feature/- +``` + +2. Open PR: `feature/...` -> `development` + +- Merge method: **Create a merge commit** + +3. After feature PR merge cleanup + +```sh +git switch development +git pull --ff-only origin development +git branch -d feature/- +git push origin --delete feature/- # if not auto-deleted +``` + +4. Promote to `staging` with PR + +- Open PR: `development` -> `staging` +- Merge method: **Create a merge commit** +- Do not squash and do not rebase this PR + +5. After `staging` promotion PR merge cleanup + +- No sync-back action needed +- Update local branches: + +```sh +git fetch origin --prune +git switch development && git pull --ff-only origin development +git switch staging && git pull --ff-only origin staging +``` + +6. Promote to `production` with PR + +- Open PR: `staging` -> `production` +- Merge method: **Create a merge commit** +- Do not squash and do not rebase this PR + +7. After `production` promotion PR merge cleanup + +- Update local branches: + +```sh +git fetch origin --prune +git switch development && git pull --ff-only origin development +git switch staging && git pull --ff-only origin staging +git switch production && git pull --ff-only origin production +``` + +8. Start next feature branch from updated `development`. + ### Declarative Database Schema (public) This project uses an incremental declarative schema workflow for the `public` schema: