feat(release): publish gitea release notes and enable production rollback

This commit is contained in:
2026-02-11 19:09:22 +01:00
parent af52b8581f
commit ccac669454
5 changed files with 170 additions and 17 deletions

View File

@@ -0,0 +1,45 @@
#!/usr/bin/env sh
set -eu
tag="${1:-}"
if [ -z "$tag" ]; then
echo "Missing release tag argument (expected vX.Y.Z)."
exit 1
fi
if [ ! -f CHANGELOG.md ]; then
echo "CHANGELOG.md not found."
exit 1
fi
version="${tag#v}"
awk -v version="$version" '
BEGIN {
in_section = 0
started = 0
}
/^## / {
if (in_section == 1) {
exit
}
if (index($0, version) > 0) {
in_section = 1
started = 1
print $0
next
}
}
{
if (in_section == 1) {
print $0
}
}
END {
if (started == 0) {
exit 2
}
}
' CHANGELOG.md

View File

@@ -0,0 +1,80 @@
import { readFileSync } from "node:fs"
const tag = process.env.RELEASE_TAG?.trim()
const releaseName = process.env.RELEASE_NAME?.trim() || tag
const bodyFile = process.env.RELEASE_BODY_FILE?.trim() || ".gitea-release-notes.md"
const serverUrl = process.env.GITHUB_SERVER_URL?.trim()
const repository = process.env.GITHUB_REPOSITORY?.trim()
const token = process.env.GITEA_RELEASE_TOKEN?.trim()
if (!tag) {
throw new Error("RELEASE_TAG is required")
}
if (!serverUrl || !repository) {
throw new Error("GITHUB_SERVER_URL and GITHUB_REPOSITORY are required")
}
if (!token) {
throw new Error("GITEA_RELEASE_TOKEN is required")
}
const body = readFileSync(bodyFile, "utf8")
const baseApi = `${serverUrl.replace(/\/$/, "")}/api/v1/repos/${repository}`
async function request(path, options = {}) {
const response = await fetch(`${baseApi}${path}`, {
...options,
headers: {
"content-type": "application/json",
authorization: `token ${token}`,
...(options.headers ?? {}),
},
})
return response
}
const payload = {
tag_name: tag,
target_commitish: "main",
name: releaseName,
body,
draft: false,
prerelease: false,
}
const existingResponse = await request(`/releases/tags/${encodeURIComponent(tag)}`)
if (existingResponse.ok) {
const existing = await existingResponse.json()
const updateResponse = await request(`/releases/${existing.id}`, {
method: "PATCH",
body: JSON.stringify({
...payload,
target_commitish: existing.target_commitish ?? payload.target_commitish,
}),
})
if (!updateResponse.ok) {
const message = await updateResponse.text()
throw new Error(`Failed to update release: ${updateResponse.status} ${message}`)
}
console.log(`Updated release for tag ${tag}`)
} else if (existingResponse.status === 404) {
const createResponse = await request("/releases", {
method: "POST",
body: JSON.stringify(payload),
})
if (!createResponse.ok) {
const message = await createResponse.text()
throw new Error(`Failed to create release: ${createResponse.status} ${message}`)
}
console.log(`Created release for tag ${tag}`)
} else {
const message = await existingResponse.text()
throw new Error(`Failed to query existing release: ${existingResponse.status} ${message}`)
}

View File

@@ -8,7 +8,7 @@ on:
inputs:
release_tag:
description: "Release tag in vX.Y.Z format"
required: true
required: false
rollback_image_tag:
description: "Optional rollback image tag"
required: false
@@ -21,6 +21,7 @@ env:
jobs:
release:
name: Build Push Changelog
if: github.event_name == 'push' || github.event.inputs.rollback_image_tag == ''
runs-on: node22-bun
steps:
- name: Checkout
@@ -38,6 +39,10 @@ jobs:
id: tag
run: |
if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
if [ -z "${{ github.event.inputs.release_tag }}" ]; then
echo "release_tag input is required when publishing a release manually."
exit 1
fi
echo "value=${{ github.event.inputs.release_tag }}" >> "$GITHUB_OUTPUT"
else
echo "value=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
@@ -49,6 +54,13 @@ jobs:
- name: Generate changelog
run: bun run changelog:release
- name: Build release notes payload
run: |
if ! sh .gitea/scripts/extract-release-notes.sh "${{ steps.tag.outputs.value }}" > .gitea-release-notes.md; then
echo "Could not isolate section for tag ${{ steps.tag.outputs.value }}. Falling back to full CHANGELOG.md."
cp CHANGELOG.md .gitea-release-notes.md
fi
- name: Login to image registry
run: |
echo "${{ secrets.CMS_IMAGE_REGISTRY_PASSWORD }}" | docker login "${{ env.REGISTRY }}" -u "${{ secrets.CMS_IMAGE_REGISTRY_USER }}" --password-stdin
@@ -65,18 +77,27 @@ jobs:
docker build -f apps/admin/Dockerfile -t "$image" .
docker push "$image"
- name: Release notes placeholder
run: |
echo "Release tag: ${{ steps.tag.outputs.value }}"
echo "TODO: publish CHANGELOG.md content to release notes in Gitea."
- name: Publish release notes to Gitea
env:
RELEASE_TAG: ${{ steps.tag.outputs.value }}
RELEASE_NAME: ${{ steps.tag.outputs.value }}
RELEASE_BODY_FILE: ".gitea-release-notes.md"
GITEA_RELEASE_TOKEN: ${{ secrets.GITEA_RELEASE_TOKEN }}
run: bun .gitea/scripts/publish-gitea-release.mjs
rollback:
name: Rollback (Manual)
name: Rollback Production (Manual)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.rollback_image_tag != ''
runs-on: ubuntu-latest
needs: release
runs-on: node22-bun
steps:
- name: Rollback placeholder
- name: Setup SSH
run: |
echo "Rollback to image tag: ${{ github.event.inputs.rollback_image_tag }}"
echo "TODO: apply compose update with rollback image tags on production host."
mkdir -p ~/.ssh
echo "${{ secrets.CMS_DEPLOY_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H "${{ secrets.CMS_PRODUCTION_HOST }}" >> ~/.ssh/known_hosts
- name: Apply rollback image tag on production
run: |
ssh "${{ secrets.CMS_PRODUCTION_USER }}@${{ secrets.CMS_PRODUCTION_HOST }}" \
"cd ${{ secrets.CMS_REMOTE_DEPLOY_PATH }} && CMS_IMAGE_TAG=${{ github.event.inputs.rollback_image_tag }} docker compose -f docker-compose.production.yml pull && CMS_IMAGE_TAG=${{ github.event.inputs.rollback_image_tag }} docker compose -f docker-compose.production.yml up -d"