From 85ec6c666d2827c0a273a428475a0d584a18f233 Mon Sep 17 00:00:00 2001 From: Foulla Date: Sat, 19 Sep 2026 19:42:12 +0100 Subject: [PATCH 1/2] ci(release): deploy production only after publication --- .github/workflows/release.yml | 37 +++++++++++++++++++++++++++++++++++ docs/deployment.md | 2 ++ docs/releases.md | 16 +++++++++++++-- render.yaml | 2 ++ 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e2e3ab8..cc04878 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,3 +61,40 @@ jobs: for pr in $prs; do gh pr edit "$pr" --repo "$GITHUB_REPOSITORY" --remove-label "autorelease: pending" done + + deploy: + name: Promote published release to production + needs: publish + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: write + concurrency: + group: production-promotion + cancel-in-progress: false + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ github.ref_name }} + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + fetch-depth: 0 + persist-credentials: false + - name: Verify publication and advance production + shell: bash + run: | + set -euo pipefail + gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" \ + --json isDraft,isPrerelease,tagName \ + --jq 'select(.isDraft == false and .isPrerelease == false) | .tagName' \ + | grep -Fx -- "$RELEASE_TAG" + release_commit=$(git rev-parse HEAD) + # Never deploy an older release over a newer one, including on retries. + git merge-base --is-ancestor origin/production "$release_commit" + if [ "$(git rev-parse origin/production)" = "$release_commit" ]; then + echo "Production already points to $RELEASE_TAG." + exit 0 + fi + gh api --method PATCH "repos/$GITHUB_REPOSITORY/git/refs/heads/production" \ + -f sha="$release_commit" -F force=false --silent + echo "Production branch advanced to $RELEASE_TAG ($release_commit). Check the hosting deployment for completion." >> "$GITHUB_STEP_SUMMARY" diff --git a/docs/deployment.md b/docs/deployment.md index 839e0f9..bac1b2f 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -1,5 +1,7 @@ # Deployment checklist +Production deployment follows published releases through the `production` branch. Configure the hosting branch before relying on this behavior; see [release setup and verification](releases.md#production-branch-setup). + This guide is for operators hosting their own Wida instance. The source is available under the [MIT License](../LICENSE); Google, Azure, and hosting services require your own configuration and may incur costs. The repository does not provision infrastructure or promise a hosted service. For the optional Render Free/Supabase deployment, use the [step-by-step configuration and secrets guide](render-free.md). Its sleep behavior and classic-queue guarantees differ from an always-running VPS. diff --git a/docs/releases.md b/docs/releases.md index d1c2577..6f5890d 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -34,6 +34,18 @@ Keep published tags immutable. If publication or label cleanup fails, inspect lo ## Deployment -A GitHub Release does not itself deploy the API. Render's existing Git integration can deploy when main changes. Verify the deployed commit and `/healthz`, and perform appropriate authenticated smoke tests before claiming an endpoint works in production. Document skipped checks honestly. Deploy backward-compatible API changes before dependent frontend features. +Production follows the `production` branch. Feature and release PRs still target `main`; merging them does not deploy production once the hosting settings below are configured. After tag CI and GitHub publication succeed, the Release workflow advances `production` to the exact tagged commit. Render then builds that commit. Verify the deployed commit and `/healthz`, and perform appropriate authenticated smoke tests before claiming an endpoint works in production. Deploy backward-compatible API releases before dependent frontend releases. -No migration, authentication configuration, production credentials or Render settings are changed by this release setup. Keep the previous successful Render deployment for rollback. Publish fixes under a new version instead of moving an existing tag. +This workflow does not change migrations, authentication configuration or production credentials. Keep the previous successful Render deployment for rollback. Publish fixes under a new version instead of moving an existing tag. + +## Production branch setup + +The `production` branch is a deployment pointer, not an integration branch. Do not merge PRs into it, push feature commits to it, or delete it during stale-branch cleanup. It starts at the last published release. Keep `main` as the repository default branch. Allow the release workflow to update `production`; do not require PR-only updates for that branch unless the workflow has an appropriate bypass. + +Set Render → wida-api → Settings → Branch to `production`, with Auto-Deploy set to On Commit. The checked-in Blueprint declares the same branch; manually created services must be updated in the dashboard. Until that setting is changed, merges to `main` can still deploy. + +Promotion runs in the existing tag workflow after publication, not a separate `release: published` workflow: events created with `GITHUB_TOKEN` do not start another ordinary Actions workflow. The existing hosting Git integration handles the branch update, so no hosting API token or deploy-hook secret is needed. + +Promotions are serialized and fast-forward only. A retry at the current production commit is a no-op; an older or divergent release fails instead of rolling production backward. If publication succeeds but promotion fails, rerun the failed job. If the host build fails after promotion, retry that exact commit in the host dashboard. For an emergency rollback, restore a previous successful deployment in the host, then publish a new patch release containing the fix or revert; never force-push production or move release tags. Manual hosting deployments and rollbacks remain operator overrides. + +For the first release after this setup, confirm the workflow's promotion succeeds and the hosting deployment is triggered for the same SHA. The workflow reports branch promotion only; host build completion and application checks must be verified separately. diff --git a/render.yaml b/render.yaml index a8c67c1..7ad1f05 100644 --- a/render.yaml +++ b/render.yaml @@ -2,6 +2,8 @@ services: - type: web name: wida-api runtime: docker + branch: production + autoDeployTrigger: commit plan: free dockerfilePath: ./Dockerfile healthCheckPath: /healthz From 61e82cd9e6c2107e9a00156af91c01fa41968e94 Mon Sep 17 00:00:00 2001 From: Foulla Date: Sat, 19 Sep 2026 19:54:58 +0100 Subject: [PATCH 2/2] ci(release): adopt GitFlow and deploy published tags --- .github/workflows/ci.yml | 2 +- .github/workflows/release-please.yml | 8 ++-- .github/workflows/release.yml | 45 +++++++++++---------- docs/deployment.md | 2 +- docs/releases.md | 59 ++++++++++++++-------------- render.yaml | 4 +- 6 files changed, 62 insertions(+), 58 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5708f47..514bce9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: CI on: pull_request: push: - branches: [main] + branches: [main, develop, "release/**", "hotfix/**"] workflow_call: workflow_dispatch: diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 882c46e..deb962f 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -2,19 +2,19 @@ name: Release Please on: push: - branches: [main] + branches: ["release/**", "hotfix/**"] workflow_dispatch: permissions: contents: read concurrency: - group: release-please-main + group: release-please-${{ github.ref }} cancel-in-progress: false jobs: prepare: - if: github.ref == 'refs/heads/main' + if: startsWith(github.ref, 'refs/heads/release/') || startsWith(github.ref, 'refs/heads/hotfix/') runs-on: ubuntu-latest timeout-minutes: 10 permissions: @@ -28,7 +28,7 @@ jobs: uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4 with: token: ${{ github.token }} - target-branch: main + target-branch: ${{ github.ref_name }} config-file: release-please-config.json manifest-file: .release-please-manifest.json skip-github-release: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cc04878..ce1cf7d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,33 +54,36 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - export RELEASE_COMMIT="$(git rev-parse HEAD)" - prs=$(gh pr list --repo "$GITHUB_REPOSITORY" --state merged --base main \ + release_commit=$(git rev-parse HEAD) + gh pr list --repo "$GITHUB_REPOSITORY" --state merged \ --label "autorelease: pending" --limit 100 --json number,mergeCommit \ - --jq '.[] | select(.mergeCommit.oid == env.RELEASE_COMMIT) | .number') - for pr in $prs; do - gh pr edit "$pr" --repo "$GITHUB_REPOSITORY" --remove-label "autorelease: pending" - done + --jq '.[] | [.number, .mergeCommit.oid] | @tsv' > "$RUNNER_TEMP/release-prs.tsv" + while IFS=$'\t' read -r pr commit; do + if [ -n "$commit" ] && git merge-base --is-ancestor "$commit" "$release_commit"; then + gh pr edit "$pr" --repo "$GITHUB_REPOSITORY" --remove-label "autorelease: pending" + fi + done < "$RUNNER_TEMP/release-prs.tsv" deploy: - name: Promote published release to production + name: Deploy published release needs: publish runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 25 permissions: - contents: write + contents: read concurrency: - group: production-promotion + group: production-deployment cancel-in-progress: false env: GH_TOKEN: ${{ github.token }} RELEASE_TAG: ${{ github.ref_name }} + RENDER_DEPLOY_HOOK_URL: ${{ secrets.RENDER_DEPLOY_HOOK_URL }} steps: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: fetch-depth: 0 persist-credentials: false - - name: Verify publication and advance production + - name: Verify published release is current main shell: bash run: | set -euo pipefail @@ -88,13 +91,15 @@ jobs: --json isDraft,isPrerelease,tagName \ --jq 'select(.isDraft == false and .isPrerelease == false) | .tagName' \ | grep -Fx -- "$RELEASE_TAG" + # A stale release retry must not roll production back. + test "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" + - name: Request Render deployment of the tagged commit + shell: bash + run: | + set -euo pipefail + : "${RENDER_DEPLOY_HOOK_URL:?Add the RENDER_DEPLOY_HOOK_URL repository secret}" release_commit=$(git rev-parse HEAD) - # Never deploy an older release over a newer one, including on retries. - git merge-base --is-ancestor origin/production "$release_commit" - if [ "$(git rev-parse origin/production)" = "$release_commit" ]; then - echo "Production already points to $RELEASE_TAG." - exit 0 - fi - gh api --method PATCH "repos/$GITHUB_REPOSITORY/git/refs/heads/production" \ - -f sha="$release_commit" -F force=false --silent - echo "Production branch advanced to $RELEASE_TAG ($release_commit). Check the hosting deployment for completion." >> "$GITHUB_STEP_SUMMARY" + curl --fail --silent --show-error --max-time 60 \ + --get --request POST --data-urlencode "ref=$release_commit" \ + "$RENDER_DEPLOY_HOOK_URL" --output "$RUNNER_TEMP/render-deploy.json" + echo "Render accepted deployment of $RELEASE_TAG ($release_commit). Verify completion and commit SHA in Render before claiming the release is live." >> "$GITHUB_STEP_SUMMARY" diff --git a/docs/deployment.md b/docs/deployment.md index bac1b2f..5e237ee 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -1,6 +1,6 @@ # Deployment checklist -Production deployment follows published releases through the `production` branch. Configure the hosting branch before relying on this behavior; see [release setup and verification](releases.md#production-branch-setup). +Production deploys only after publication of a release tag. Feature work integrates on `develop`; `main` holds release history. Configure hosting and GitHub secrets using [the GitFlow release guide](releases.md#hosting-setup). This guide is for operators hosting their own Wida instance. The source is available under the [MIT License](../LICENSE); Google, Azure, and hosting services require your own configuration and may incur costs. The repository does not provision infrastructure or promise a hosted service. diff --git a/docs/releases.md b/docs/releases.md index 6f5890d..c019b0a 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1,51 +1,50 @@ -# API releases +# API releases with GitFlow -The API and frontend have independent versions and tags. Release Please uses the `simple` strategy to update `version.txt`, `.release-please-manifest.json` and `CHANGELOG.md`. `version.txt` is release metadata, not an override of .NET assembly versions or Docker image tags. +The API and frontend have independent versions. Production deploys from published release tags, not ordinary PR merges. GitFlow uses `develop` for integration and `main` for release history. -## First release +## Branches and merge rules -The API release history starts at `0.1.0`. `0.0.0` was an unpublished bootstrap marker. The bootstrap commit `36a04fdb52708c6a82cc627adeaf1e407272e379` excludes earlier history while including the already-deployed document deletion change. Do not tag `0.0.0` or invent historical release dates. The bootstrap SHA only applies before the first published release. +- `feature/*` starts from `develop` and returns to `develop` through a PR. Use Conventional Commit titles (`feat:`, `fix:`) and squash feature PRs. +- `release/*` starts from `develop` when the release scope is frozen. Only stabilization fixes and release metadata belong here; new features continue on `develop`. +- `hotfix/*` starts from `main` for urgent production fixes. +- Merge a completed release or hotfix into `main` with **Create a merge commit**, not squash or rebase. After publication, merge `main` back into `develop` with a merge commit, and into any active release branch as needed. This preserves ancestry and carries fixes, version files and changelog updates forward. +- Keep `main` and `develop` permanently. Delete feature/release/hotfix branches only after their work is merged back. There is no `production` branch. -## Automated preparation +Protect both long-lived branches with CI and reviews appropriate to the team. Ensure merge commits are enabled for release and back-merge PRs; a linear-history rule conflicts with this GitFlow history. The repository can retain `main` as its default branch; always select `develop` explicitly for feature PRs. -1. Allow GitHub Actions to create pull requests in this API repository's Settings → Actions → General. This setting is independent from the frontend repository. -2. Use Conventional Commit titles and squash-merge normal PRs. `feat:` increments the minor version; `fix:` increments the patch version. Breaking changes increment minor versions during `0.x`, and major versions after `1.0`. Hidden maintenance commits do not independently request a release. -3. On pushes to `main`, Release Please opens or updates a release PR. Manual retry is available through Actions → Release Please → Run workflow on `main`. -4. The workflow uses `GITHUB_TOKEN` and explicitly dispatches CI on the generated release branch because bot-created PRs do not automatically trigger PR workflows. No personal token or production secret is required. Empty PR outputs are handled safely. -5. Review the proposed version and notes. Move applicable `[Unreleased]` entries into the generated version section, remove duplicates and retain deployment caveats. Finish editorial changes after the last feature merge, as the bot can regenerate notes. +## Prepare a release -CI checks release metadata, restores/builds the .NET 10 solution, runs the API tests and publishes the API into a temporary directory. PostgreSQL/RabbitMQ integration tests remain skipped unless their documented `WIDA_QUEUE_TEST_POSTGRES` and `WIDA_QUEUE_TEST_RABBITMQ` test-service environment variables are configured. CI never uses the production services. +1. Finish feature PRs into `develop`, then create a release branch (for example `release/next`) from its tested tip and push it. Keep only one normal release in preparation at a time. +2. The Release Please workflow runs on `release/*` and `hotfix/*`, with that branch as its target. It opens a metadata PR **into the release/hotfix branch**, not into `develop` or `main`. It does not run on ordinary pushes to either long-lived branch. +3. Review the generated version and dated changelog. Conventional `feat:` and `fix:` commits determine the version; branch names do not set it. Add compatibility notes and move relevant Unreleased entries into the version section. Merge the metadata PR into the release branch once its CI passes. Do not manually publish or tag the metadata PR commit. +4. Stabilize and test the release branch, then open its PR into `main`. Finish stabilization before merging the metadata PR when possible. If fixes are added afterward, update that version’s changelog on the release branch before finishing; a merged metadata PR remains pending until publication. +5. Merge the release PR into `main` with a merge commit. Wait for CI on that exact merge commit. Do not merge the next release into `main` until this one's deployment is complete. -## Validate, tag and publish +Release Please uses `GITHUB_TOKEN` and `skip-github-release: true`. Its generated metadata PR explicitly dispatches CI because token-created PRs do not trigger ordinary PR workflows. Manual preparation is available from Actions → Release Please → Run workflow, selecting the release/hotfix branch. Enable GitHub Actions PR creation in repository settings. -Release Please prepares PRs only (`skip-github-release: true`). After CI passes, merge the reviewed release PR under the repository's normal protections. Wait for main CI on that exact merge commit. +## Publish and deploy ```sh git fetch origin main --tags -node scripts/check-release.mjs -node scripts/release-notes.mjs vX.Y.Z -git tag -a vX.Y.Z -m "Wida API vX.Y.Z" +# Use the exact release merge SHA whose CI passed, not an arbitrary later main commit. +git tag -a vX.Y.Z -m "Wida API vX.Y.Z" git push origin vX.Y.Z ``` -The tag workflow reruns CI, requires a commit on main, verifies the tag against `version.txt`, extracts only that version's dated changelog entry and publishes a GitHub Release. It then removes the matching release PR's `autorelease: pending` label so preparation can continue. The unpublished `0.0.0` marker has no dated notes and cannot pass publication. +The tag workflow reruns CI, verifies main ancestry, validates the version and dated changelog, and publishes the GitHub Release. It clears pending labels on merged Release Please metadata PRs included in the tag. A deployment job then checks the release is published (not draft/prerelease) and that the tagged SHA is still the current main tip before deploying that exact revision. A release event generated with `GITHUB_TOKEN` cannot trigger another ordinary Actions workflow, so deployment is part of the same tag workflow after publication. -Keep published tags immutable. If publication or label cleanup fails, inspect logs and rerun: existing published releases are reused, while existing drafts require explicit attention. If tagging a later commit that supersedes an unpublished release PR, reconcile that older PR's pending label only after successful publication. +Never move published tags. For a transient publication or deployment failure, rerun the failed job for the same tag. Deployment jobs are serialized. An older release whose SHA no longer equals main fails instead of rolling production back. A retry may create another hosting deployment of the same commit. For emergency rollback, restore the previous successful deployment in the host dashboard, then ship a hotfix release; do not force-push main or reuse a tag. -## Deployment +After publishing, open `main` → `develop` and merge it with a merge commit. Merge hotfixes into an active release branch too. This back-merge is required before preparing the next release so Release Please sees the previous version and history. -Production follows the `production` branch. Feature and release PRs still target `main`; merging them does not deploy production once the hosting settings below are configured. After tag CI and GitHub publication succeed, the Release workflow advances `production` to the exact tagged commit. Render then builds that commit. Verify the deployed commit and `/healthz`, and perform appropriate authenticated smoke tests before claiming an endpoint works in production. Deploy backward-compatible API releases before dependent frontend releases. +## Hosting setup -This workflow does not change migrations, authentication configuration or production credentials. Keep the previous successful Render deployment for rollback. Publish fixes under a new version instead of moving an existing tag. +1. Render → wida-api → Settings: keep Branch `main` and set **Auto-Deploy → Off**. The Blueprint also declares `branch: main` and `autoDeployTrigger: off`; manually created services need the dashboard change. +2. Copy the Render service's Deploy Hook URL into the API repository's GitHub Actions secret `RENDER_DEPLOY_HOOK_URL`. Do not commit it or paste it in a PR. +3. The release job calls the hook with `ref=`. A successful hook response means accepted/queued, not that the service is live. Verify Render's deployed SHA and `/healthz`, then appropriate authenticated smoke tests. -## Production branch setup +See [Render deploy hooks](https://render.com/docs/deploy-hooks). No database or authentication settings change. The API's PostgreSQL/RabbitMQ integration tests remain skipped unless their documented test-service variables are configured. Release the backward-compatible API before a dependent frontend release. -The `production` branch is a deployment pointer, not an integration branch. Do not merge PRs into it, push feature commits to it, or delete it during stale-branch cleanup. It starts at the last published release. Keep `main` as the repository default branch. Allow the release workflow to update `production`; do not require PR-only updates for that branch unless the workflow has an appropriate bypass. +## Activating this setup -Set Render → wida-api → Settings → Branch to `production`, with Auto-Deploy set to On Commit. The checked-in Blueprint declares the same branch; manually created services must be updated in the dashboard. Until that setting is changed, merges to `main` can still deploy. - -Promotion runs in the existing tag workflow after publication, not a separate `release: published` workflow: events created with `GITHUB_TOKEN` do not start another ordinary Actions workflow. The existing hosting Git integration handles the branch update, so no hosting API token or deploy-hook secret is needed. - -Promotions are serialized and fast-forward only. A retry at the current production commit is a no-op; an older or divergent release fails instead of rolling production backward. If publication succeeds but promotion fails, rerun the failed job. If the host build fails after promotion, retry that exact commit in the host dashboard. For an emergency rollback, restore a previous successful deployment in the host, then publish a new patch release containing the fix or revert; never force-push production or move release tags. Manual hosting deployments and rollbacks remain operator overrides. - -For the first release after this setup, confirm the workflow's promotion succeeds and the hosting deployment is triggered for the same SHA. The workflow reports branch promotion only; host build completion and application checks must be verified separately. +Disable the host's automatic production deploys and add the required repository secrets before publishing the next release. Merge this infrastructure setup into main, then merge main into develop with a merge commit so future feature/release branches inherit it. This one-time setup PR is the exception to normal release-only main merges. Existing published tags contain the old workflow; rerunning them does not use this new deployment job. Verify the next newly published release end to end before declaring automated release deployment operational. diff --git a/render.yaml b/render.yaml index 7ad1f05..e70503a 100644 --- a/render.yaml +++ b/render.yaml @@ -2,8 +2,8 @@ services: - type: web name: wida-api runtime: docker - branch: production - autoDeployTrigger: commit + branch: main + autoDeployTrigger: "off" plan: free dockerfilePath: ./Dockerfile healthCheckPath: /healthz