diff --git a/.github/workflows/back-merge-pr.yml b/.github/workflows/back-merge-pr.yml new file mode 100644 index 0000000..cec0f26 --- /dev/null +++ b/.github/workflows/back-merge-pr.yml @@ -0,0 +1,59 @@ +# Opens a PR from master → development after changes land on master (back-merge). +# +# Org/repo Settings → Actions → General → Workflow permissions: read and write +# (so GITHUB_TOKEN can create pull requests). Or use a PAT in secret GH_TOKEN. + +name: Back-merge master to development + +on: + push: + branches: [master] + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + +jobs: + open-back-merge-pr: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Open back-merge PR if needed + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + git fetch origin development master + + MASTER_SHA=$(git rev-parse origin/master) + DEV_SHA=$(git rev-parse origin/development) + + if [ "$MASTER_SHA" = "$DEV_SHA" ]; then + echo "master and development are at the same commit; nothing to back-merge." + exit 0 + fi + + EXISTING=$(gh pr list --repo "${{ github.repository }}" \ + --base development \ + --head master \ + --state open \ + --json number \ + --jq 'length') + + if [ "$EXISTING" -gt 0 ]; then + echo "An open PR from master to development already exists; skipping." + exit 0 + fi + + gh pr create --repo "${{ github.repository }}" \ + --base development \ + --head master \ + --title "chore: back-merge master into development" \ + --body "Automated back-merge after changes landed on \`master\`. Review and merge to keep \`development\` in sync." + + echo "Created back-merge PR master → development." diff --git a/.github/workflows/check-branch.yml b/.github/workflows/check-branch.yml deleted file mode 100644 index 2332f0d..0000000 --- a/.github/workflows/check-branch.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: 'Check Branch' - -on: - pull_request: - -jobs: - check_branch: - runs-on: ubuntu-latest - steps: - - name: Comment PR - if: github.base_ref == 'master' && github.head_ref != 'staging' - uses: thollander/actions-comment-pull-request@v2 - with: - message: | - We regret to inform you that you are currently not able to merge your changes into the master branch due to restrictions applied by our SRE team. To proceed with merging your changes, we kindly request that you create a pull request from the staging branch. Our team will then review the changes and work with you to ensure a successful merge into the master branch. - - name: Check branch - if: github.base_ref == 'master' && github.head_ref != 'staging' - run: | - echo "ERROR: We regret to inform you that you are currently not able to merge your changes into the master branch due to restrictions applied by our SRE team. To proceed with merging your changes, we kindly request that you create a pull request from the staging branch. Our team will then review the changes and work with you to ensure a successful merge into the master branch." - exit 1 diff --git a/.github/workflows/check-version-bump.yml b/.github/workflows/check-version-bump.yml new file mode 100644 index 0000000..fc44096 --- /dev/null +++ b/.github/workflows/check-version-bump.yml @@ -0,0 +1,76 @@ +# Runs only when production code under lib/ changes. Version must be > latest v* tag (not vs base branch). + +name: Check Version Bump + +on: + pull_request: + +jobs: + check-version-bump: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Validate version and changelog updates + shell: bash + run: | + set -euo pipefail + + VERSION_FILE="pubspec.yaml" + CHANGELOG_FILE="CHANGELOG.md" + BASE_SHA="${{ github.event.pull_request.base.sha }}" + HEAD_SHA="${{ github.event.pull_request.head.sha }}" + + mapfile -t CHANGED_FILES < <(git diff --name-only "$BASE_SHA" "$HEAD_SHA") + if [ "${#CHANGED_FILES[@]}" -eq 0 ]; then + echo "No changed files detected." + exit 0 + fi + + is_production_source_change() { + local f="$1" + [[ "$f" == lib/* ]] + } + + has_source_changes=false + for file in "${CHANGED_FILES[@]}"; do + if is_production_source_change "$file"; then + has_source_changes=true + break + fi + done + + if [ "$has_source_changes" = false ]; then + echo "Skipping: no lib/ production code changes." + exit 0 + fi + + changed_file() { + local target="$1" + for file in "${CHANGED_FILES[@]}"; do + if [ "$file" = "$target" ]; then + return 0 + fi + done + return 1 + } + + changed_file "$VERSION_FILE" || { echo "Version bump required in $VERSION_FILE."; exit 1; } + changed_file "$CHANGELOG_FILE" || { echo "Matching changelog update required in $CHANGELOG_FILE."; exit 1; } + + head_version=$(sed -nE 's/^version:[[:space:]]*([^[:space:]]+).*/\1/p' "$VERSION_FILE" | sed -n '1p') + CHANGELOG_HEAD=$(sed -nE 's/^## v?([^[:space:]]+).*/\1/p' "$CHANGELOG_FILE" | head -1) + + [ -n "$CHANGELOG_HEAD" ] || { echo "::error::Could not find a top changelog heading like '## vX.Y.Z' in $CHANGELOG_FILE."; exit 1; } + [ "$CHANGELOG_HEAD" = "$head_version" ] || { echo "::error::$CHANGELOG_FILE top version ($CHANGELOG_HEAD) does not match project version ($head_version)."; exit 1; } + + latest_tag=$(git tag --list 'v*' --sort=-version:refname | sed -n '1p') + latest_version="${latest_tag#v}" + [ -n "$latest_version" ] || latest_version="0.0.0" + + version_gt() { + python3 -c 'import sys;v=lambda s:[int(x) if x.isdigit() else 0 for x in (s.strip().lstrip("v").split("-",1)[0].split("+",1)[0].split(".")+["0","0","0"])[:3]];print("true" if v(sys.argv[1])>v(sys.argv[2]) else "false")' "$1" "$2" + } + + [ "$(version_gt "$head_version" "$latest_version")" = "true" ] || { echo "Version must be greater than latest tag version ($latest_version). Found $head_version."; exit 1; } diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fca0c29..451216d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,13 +2,13 @@ name: Publish to pub.dev on: - push: - tags: - - 'v[0-9]+.[0-9]+.[0-9]+' # must match tag pattern on pub.dev (e.g. 'v{{version}}') - workflow_dispatch: # manual trigger: runs dry-run only (pub.dev accepts publish only on tag push) + release: + types: [created] + workflow_dispatch: jobs: publish: + if: ${{ github.event_name == 'release' && startsWith(github.event.release.tag_name, 'v') && !github.event.release.draft }} permissions: id-token: write # Required for OIDC authentication name: 'Publish to pub.dev' @@ -16,6 +16,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.tag_name }} - uses: dart-lang/setup-dart@v1 with: sdk: stable @@ -24,4 +26,18 @@ jobs: - name: Test Publish (Dry Run) run: dart pub publish --dry-run - name: Publish - run: dart pub publish --force \ No newline at end of file + run: dart pub publish --force + + publish-dry-run: + if: ${{ github.event_name == 'workflow_dispatch' }} + name: Publish dry-run (manual) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dart-lang/setup-dart@v1 + with: + sdk: stable + - name: Install dependencies + run: dart pub get + - name: Test Publish (Dry Run) + run: dart pub publish --dry-run diff --git a/AGENTS.md b/AGENTS.md index 859da6e..92bb447 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,7 +28,7 @@ | **Test** | `dart test` | | **Lint** | `dart analyze .` and `dart format .` | -**CI:** branch policy — `.github/workflows/check-branch.yml`; SCA — `.github/workflows/sca-scan.yml`; publish on tags — `.github/workflows/publish.yml`. +**CI:** feature/fix PRs target `development`, release PRs target `master` from `development`; SCA — `.github/workflows/sca-scan.yml`; publish on GitHub **Release** (`release: created`, tag `v*`) via `.github/workflows/publish.yml`; manual **workflow_dispatch** runs dry-run only; back-merge automation — `.github/workflows/back-merge-pr.yml`. ## Where the documentation lives: skills diff --git a/skills/dev-workflow/SKILL.md b/skills/dev-workflow/SKILL.md index 4491cc7..6d7a9f7 100644 --- a/skills/dev-workflow/SKILL.md +++ b/skills/dev-workflow/SKILL.md @@ -17,7 +17,7 @@ description: Use when branching, running CI-related commands, opening PRs, or de ### Branches - Use feature branches (e.g. `feat/...`, `fix/...`). -- **`.github/workflows/check-branch.yml`** blocks PRs into **`master`** unless the head branch is **`staging`**. Prefer PRs against **`staging`** per team policy. +- Feature/fix PRs should target **`development`**. Release PRs are raised directly from **`development`** to **`master`**. ### Commands @@ -40,7 +40,7 @@ Run **`dart analyze .`** and **`dart test`** before requesting review. There are ### Publishing (maintainers) -- **`.github/workflows/publish.yml`** — publish on tags matching `v*.*.*` via **`dart pub publish`** (after dry-run in workflow). +- **`.github/workflows/publish.yml`** — on **`release: types: [created]`** for tag **`v*`** (draft releases skipped), checks out the tag, then **`dart pub publish --dry-run`** and **`dart pub publish`**. **Manual** `workflow_dispatch` runs **dry-run only** (separate job). ### Optional TDD diff --git a/skills/framework/SKILL.md b/skills/framework/SKILL.md index 8e170f8..7a9d3aa 100644 --- a/skills/framework/SKILL.md +++ b/skills/framework/SKILL.md @@ -35,7 +35,7 @@ This package has **no** HTTP client, retries, or native modules — only Pub, an ### Publishing -- **`.github/workflows/publish.yml`:** tag pattern **`v*.*.*`**, **`dart pub publish`** after dry-run. +- **`.github/workflows/publish.yml`:** GitHub **Release** (`release: created`) for tag **`v*`**; **`dart pub publish`** after dry-run. Manual trigger: dry-run job only. ## References