Files
FOSSBilling/.github/workflows/create-release.yml

280 lines
10 KiB
YAML

name: Create Release
permissions:
contents: write
on:
workflow_dispatch:
inputs:
version:
description: 'Release version, for example 0.7.3'
required: true
type: string
target_ref:
description: 'Git ref to build and tag'
required: true
default: main
type: string
draft:
description: 'Create as a draft release'
required: true
default: true
type: boolean
prerelease:
description: 'Mark as a prerelease'
required: true
default: false
type: boolean
release_notes:
description: 'Release notes mode; copilot uses BYOK provider settings'
required: true
default: github
type: choice
options:
- copilot
- github
- none
concurrency:
group: create-release-${{ inputs.version }}
cancel-in-progress: false
jobs:
create-release:
name: 'Create Release'
if: ${{ github.repository == 'FOSSBilling/FOSSBilling' }}
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: 'Validate Release Inputs'
env:
RELEASE_VERSION: ${{ inputs.version }}
TARGET_REF: ${{ inputs.target_ref }}
run: |
if [[ ! "${RELEASE_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then
echo "Release version must be valid SemVer without a leading v."
exit 1
fi
if [[ ! "${TARGET_REF}" =~ ^[A-Za-z0-9._/-]+$ ]] || ! git check-ref-format --branch "${TARGET_REF}" >/dev/null 2>&1; then
echo "Target ref must be a valid branch name containing only letters, numbers, dots, underscores, slashes, or hyphens."
exit 1
fi
- name: 'Checkout Repository'
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 0
ref: ${{ inputs.target_ref }}
persist-credentials: false
- name: 'Prepare Release Notes Context'
id: notes-context
if: ${{ inputs.release_notes != 'none' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_VERSION: ${{ inputs.version }}
REPOSITORY: ${{ github.repository }}
run: |
mkdir -p release-notes-context
previous_tag="$(
git tag --merged HEAD --sort=-v:refname \
| grep -Ev "^${RELEASE_VERSION}$" \
| head -n 1
)"
if [[ -z "${previous_tag}" ]]; then
echo "Could not determine a previous tag to diff from."
exit 1
fi
echo "previous_tag=${previous_tag}" >> "${GITHUB_OUTPUT}"
gh api "repos/${REPOSITORY}/releases" \
--jq '.[0:5] | map({tag_name, name, draft, prerelease, published_at, body})' \
> release-notes-context/recent-releases.json
if gh api "repos/${REPOSITORY}/releases/tags/${RELEASE_VERSION}" > release-notes-context/current-release.json 2>/dev/null; then
jq -r '.body // ""' release-notes-context/current-release.json > release-notes-context/current-release-notes.md
else
printf '{}\n' > release-notes-context/current-release.json
printf '\n' > release-notes-context/current-release-notes.md
fi
git log --reverse --date=short \
--pretty=format:'%h %ad %an%n%s%n%b%n---END-COMMIT---' \
"${previous_tag}..HEAD" \
> release-notes-context/git-log.txt
gh api "repos/${REPOSITORY}/releases/generate-notes" \
-f tag_name="${RELEASE_VERSION}" \
-f target_commitish="$(git rev-parse HEAD)" \
-f previous_tag_name="${previous_tag}" \
--jq '.body' \
> release-notes-context/github-generated-notes.md
- name: 'Generate GitHub Release Notes'
if: ${{ inputs.release_notes == 'github' }}
run: cp release-notes-context/github-generated-notes.md release-notes.md
- name: 'Set up Node.js for Copilot CLI'
if: ${{ inputs.release_notes == 'copilot' }}
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24
cache: 'npm'
- name: 'Install Copilot CLI'
if: ${{ inputs.release_notes == 'copilot' }}
run: npm install -g @github/copilot@1.0.56
- name: 'Generate Curated Release Notes'
if: ${{ inputs.release_notes == 'copilot' }}
env:
COPILOT_MODEL: ${{ vars.COPILOT_MODEL }}
COPILOT_OFFLINE: true
COPILOT_PROVIDER_API_KEY: ${{ secrets.COPILOT_PROVIDER_API_KEY }}
COPILOT_PROVIDER_BASE_URL: ${{ vars.COPILOT_PROVIDER_BASE_URL }}
COPILOT_PROVIDER_TYPE: ${{ vars.COPILOT_PROVIDER_TYPE }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_VERSION: ${{ inputs.version }}
PREVIOUS_TAG: ${{ steps.notes-context.outputs.previous_tag }}
REPOSITORY: ${{ github.repository }}
run: |
if [[ -z "${COPILOT_PROVIDER_BASE_URL}" || -z "${COPILOT_MODEL}" ]]; then
echo "The copilot release notes mode requires COPILOT_PROVIDER_BASE_URL and COPILOT_MODEL Actions variables."
exit 1
fi
cat > release-notes-context/prompt.md <<EOF
Generate polished FOSSBilling release notes in release-notes.md.
Release version: ${RELEASE_VERSION}
Previous tag: ${PREVIOUS_TAG}
Inputs are in release-notes-context/:
- release-notes-guidelines.md: project release-note style and section rules.
- recent-releases.json: the five most recent releases or drafts for tone and structure.
- current-release-notes.md: existing notes for this release or draft, if any.
- github-generated-notes.md: GitHub's generated baseline for this tag range.
- git-log.txt: commits from the previous tag to the target revision.
Requirements:
- Diff from ${PREVIOUS_TAG} to ${RELEASE_VERSION}.
- Use existing current-release-notes.md as context, but rewrite it if needed.
- Follow release-notes-guidelines.md.
- Preserve accurate PR numbers, issue references, and security-sensitive wording.
- Group user-facing changes by the guideline sections.
- Remove duplicates and omit internal noise that does not help users upgrade or understand the release.
- Do not invent changes.
- Write only the final Markdown body to release-notes.md.
EOF
cp .github/release-notes-guidelines.md release-notes-context/release-notes-guidelines.md
copilot -p "$(cat release-notes-context/prompt.md)" \
--allow-tool='shell(cat:*)' \
--allow-tool=write \
--no-ask-user
- name: 'Set Registry Cache Image'
run: echo "CACHE_IMAGE=ghcr.io/${GITHUB_REPOSITORY@L}-build-cache" >> "${GITHUB_ENV}"
- name: 'Set up Docker Buildx'
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: 'Build Release Artifact'
id: build
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
with:
build-args: |
FOSSBILLING_VERSION=${{ inputs.version }}
SENTRY_DSN=${{ vars.SENTRY_DSN }}
cache-from: type=registry,ref=${{ env.CACHE_IMAGE }}:cache-release
context: .
target: release-artifact
outputs: type=local,dest=./release-tree
provenance: false
- name: 'Create Release Archive'
id: archive
env:
RELEASE_VERSION: ${{ inputs.version }}
run: |
mkdir -p ../dist
zip -qr "../dist/FOSSBilling-${RELEASE_VERSION}.zip" .
working-directory: ./release-tree
- name: 'Create or Update GitHub Release'
id: release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_VERSION: ${{ inputs.version }}
TARGET_REF: ${{ inputs.target_ref }}
IS_DRAFT: ${{ inputs.draft }}
IS_PRERELEASE: ${{ inputs.prerelease }}
RELEASE_NOTES: ${{ inputs.release_notes }}
REPOSITORY: ${{ github.repository }}
run: |
asset="./dist/FOSSBilling-${RELEASE_VERSION}.zip"
if gh release view "${RELEASE_VERSION}" --repo "${REPOSITORY}" >/dev/null 2>&1; then
gh release upload "${RELEASE_VERSION}" "${asset}" \
--repo "${REPOSITORY}" \
--clobber
if [[ "${RELEASE_NOTES}" != "none" ]]; then
gh release edit "${RELEASE_VERSION}" \
--repo "${REPOSITORY}" \
--notes-file release-notes.md
fi
echo "operation=updated" >> "${GITHUB_OUTPUT}"
exit 0
fi
release_args=(
"${RELEASE_VERSION}"
"${asset}"
--repo "${REPOSITORY}"
--title "${RELEASE_VERSION}"
--target "${TARGET_REF}"
)
if [[ "${RELEASE_NOTES}" != "none" ]]; then
release_args+=(--notes-file release-notes.md)
else
release_args+=(--notes "")
fi
if [[ "${IS_DRAFT}" == "true" ]]; then
release_args+=(--draft)
fi
if [[ "${IS_PRERELEASE}" == "true" ]]; then
release_args+=(--prerelease)
fi
gh release create "${release_args[@]}"
echo "operation=created" >> "${GITHUB_OUTPUT}"
- name: 'Write Job Summary'
if: always()
run: |
cat >> "${GITHUB_STEP_SUMMARY}" <<'EOF'
## Create Release
| Item | Value |
| --- | --- |
| Version | `${{ inputs.version }}` |
| Target ref | `${{ inputs.target_ref }}` |
| Draft | `${{ inputs.draft }}` |
| Prerelease | `${{ inputs.prerelease }}` |
| Release notes | `${{ inputs.release_notes }}` |
| Previous tag | `${{ steps.notes-context.outputs.previous_tag || 'n/a' }}` |
| Archive | `FOSSBilling-${{ inputs.version }}.zip` |
| Docker build | `${{ steps.build.outcome }}` |
| Archive creation | `${{ steps.archive.outcome }}` |
| Release operation | `${{ steps.release.outputs.operation || steps.release.outcome }}` |
EOF