diff --git a/.github/workflows/deploy-prod-1-propose.yml b/.github/workflows/deploy-prod-1-propose.yml index 30b392c9..4d36c4ab 100644 --- a/.github/workflows/deploy-prod-1-propose.yml +++ b/.github/workflows/deploy-prod-1-propose.yml @@ -15,6 +15,12 @@ # 2. Provision compose file update via Phala Cloud API → get compose hash # 3. Propose addComposeHash(hash) to Safe multisig on Base # 4. Create draft GitHub release with deployment context for Phase 2 +# 5. Create/advance the release-line branch (release/vX.Y) for hotfixes +# +# Release-line branches (CPL-358): each tagged prod release lands its commit on a +# minor-version branch release/vX.Y. All patch releases on a line share one branch, +# giving hotfixes to a shipped version a stable home that never moves as main +# advances. See architectureDocs/deployment/vm-code-upgrade.md ("Release-line branches & hotfixes"). # # Image provenance (DR-1b): Each image is signed with Sigstore (cosign keyless). # To verify: @@ -549,3 +555,60 @@ jobs: --title "${{ github.ref_name }}" \ --generate-notes \ --target "${{ github.sha }}" + + # Land the released commit on a minor-version release-line branch (release/vX.Y) + # so hotfixes to a shipped version have a stable home that never moves as main + # advances (CPL-358). All patch releases on a line (v1.2.0, v1.2.1, ...) share + # one branch. Runs only for tag pushes; workflow_dispatch re-provisions an + # existing release and has no tag to key a line off. + create-release-branch: + needs: [create-draft-release] + if: ${{ github.ref_type == 'tag' }} + runs-on: self-hosted + permissions: + contents: write # push the release-line branch + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.sha }} + fetch-depth: 0 # need history to test fast-forward against an existing branch + + - name: Create or advance the release-line branch + env: + TAG: ${{ github.ref_name }} + SHA: ${{ github.sha }} + run: | + set -euo pipefail + + # Derive release/vMAJOR.MINOR from the tag. Prefix-match tolerates + # pre-release suffixes (v1.2.0-rc1 → release/v1.2), keeping the whole + # 1.2 line on one branch. + if [[ ! "$TAG" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then + echo "::notice::Tag '$TAG' is not vMAJOR.MINOR.PATCH — skipping release-branch creation." + exit 0 + fi + BRANCH="release/v${BASH_REMATCH[1]}.${BASH_REMATCH[2]}" + echo "Release-line branch: $BRANCH (tag $TAG @ $SHA)" + + if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then + # Branch exists — an earlier patch release on this line, or a branch + # already carrying hotfix commits. Only fast-forward it to the tagged + # commit; never force-push, which would discard those hotfix commits. + # Compare against FETCH_HEAD: `git fetch` always writes it, whereas the + # origin/$BRANCH remote-tracking ref may not exist under checkout's + # narrow fetch refspec. + git fetch origin "$BRANCH" + if git merge-base --is-ancestor FETCH_HEAD "$SHA"; then + echo "Fast-forwarding $BRANCH to $SHA" + if ! git push origin "$SHA:refs/heads/$BRANCH"; then + echo "::warning::Failed to push $BRANCH to $SHA (branch may have moved). Leaving it untouched." + fi + else + echo "::warning::$BRANCH has diverged from $SHA (likely carries hotfix commits). Leaving it untouched — reconcile manually if this release should advance the line." + fi + else + echo "Creating $BRANCH at $SHA" + if ! git push origin "$SHA:refs/heads/$BRANCH"; then + echo "::warning::Failed to create $BRANCH at $SHA." + fi + fi diff --git a/architectureDocs/deployment/vm-code-upgrade.md b/architectureDocs/deployment/vm-code-upgrade.md index de6c09db..79864ccc 100644 --- a/architectureDocs/deployment/vm-code-upgrade.md +++ b/architectureDocs/deployment/vm-code-upgrade.md @@ -77,6 +77,27 @@ If the CVM boots before the whitelist tx is finalized, the KMS will reject the k the CVM cannot serve traffic. The diagram shows the whitelist flowing into the key issuance check to reflect this dependency. +## Release-line branches & hotfixes + +Every tagged production release (`v*` push → **Deploy Prod 1: Propose Compose Hash**) also +lands its commit on a minor-version **release-line branch** `release/vMAJOR.MINOR` (created by the +`create-release-branch` job). All patch releases on a line (`v1.2.0`, `v1.2.1`, …) share one +branch, so a shipped version has a stable home that does not move as `main` advances. + +To ship a hotfix to a released version without dragging in unreleased work from `main`: + +1. Fetch and branch from the release line: + `git fetch origin && git switch -c release/v1.2 origin/release/v1.2` (or from the tag: + `git fetch origin --tags && git switch --detach v1.2.3`). +2. Apply the fix (cherry-pick from `main` where possible so the fix also lands there). +3. Tag the patch and push: `git tag v1.2.4 && git push origin v1.2.4` — this re-runs the prod + propose workflow for the hotfix commit. +4. The `create-release-branch` job **fast-forwards** `release/v1.2` to the hotfix commit. It never + force-pushes: if the branch has diverged from the tagged commit it is left untouched and the run + logs a warning, so hotfix commits already on the branch are never discarded. + +Don't forget to forward-port the fix to `main` if you branched from the tag directly. + ## Rollback To roll back, redeploy the previous image (whose compose-hash is already whitelisted). No