name: Unbound Release on: workflow_call: inputs: tag_only: description: 'Set to true to only create tags without full releases' required: false default: false type: boolean secrets: UNBOUND_RELEASE_TOKEN: description: 'Token with API access to create PRs and releases' required: true env: GITEA_URL: https://git.unbound.se jobs: preconditions: name: Check Preconditions runs-on: ubuntu-latest container: image: amd64/alpine:3.22.2@sha256:b687e78c6e2785808446f45b52f1540a1e58adc07bdcffea354933b18c613d90 steps: - name: Validate token if: ${{ secrets.UNBOUND_RELEASE_TOKEN == '' }} run: | echo "To use Unbound Release, a UNBOUND_RELEASE_TOKEN secret needs to be defined." echo "It needs API access to write repository files, create PRs and releases." echo " " echo "Create a token in Gitea: Settings -> Applications -> Generate New Token" echo "Required scopes: repository (read/write), issue (read/write)" exit 1 changelog: name: Generate Changelog runs-on: ubuntu-latest needs: preconditions if: github.ref_type == 'branch' && github.ref_name == github.event.repository.default_branch container: image: orhunp/git-cliff:2.10.1@sha256:6ba0d1fcb051bd7b154cfb19c4b2b3bfa2c22c475f5285fc30606777b6573119 outputs: version: ${{ steps.version.outputs.version }} has_changes: ${{ steps.check.outputs.has_changes }} steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Generate changelog run: | git-cliff --bump --unreleased --strip header > CHANGES.md git-cliff --bump | sed "s/[[:space:]]\+$//" > CHANGELOG.md - name: Get bumped version id: version run: | VERSION=$(git-cliff --bumped-version 2>/dev/null || echo "") echo "version=${VERSION}" >> $GITHUB_OUTPUT echo "${VERSION}" > VERSION - name: Check for changes id: check run: | LATEST=$(cat .version 2>/dev/null | jq -r '.version' 2>/dev/null || git describe --abbrev=0 --tags 2>/dev/null || echo '') VERSION=$(cat VERSION) if [ -n "${LATEST}" ] && [ "${VERSION}" = "${LATEST}" ]; then echo "has_changes=false" >> $GITHUB_OUTPUT else echo "has_changes=true" >> $GITHUB_OUTPUT fi - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: changelog-artifacts path: | CHANGES.md CHANGELOG.md VERSION handle-pr: name: Handle Release PR runs-on: ubuntu-latest needs: changelog if: needs.changelog.outputs.has_changes == 'true' container: image: amd64/alpine:3.22.2@sha256:b687e78c6e2785808446f45b52f1540a1e58adc07bdcffea354933b18c613d90 steps: - name: Install dependencies run: apk add --no-cache git jq curl - name: Download artifacts uses: actions/download-artifact@v4 with: name: changelog-artifacts - name: Create or update release PR env: TOKEN: ${{ secrets.UNBOUND_RELEASE_TOKEN }} REPOSITORY: ${{ github.repository }} DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} run: | VERSION=$(cat VERSION) OWNER=$(echo "${REPOSITORY}" | cut -d'/' -f1) REPO=$(echo "${REPOSITORY}" | cut -d'/' -f2) API_URL="${GITEA_URL}/api/v1/repos/${OWNER}/${REPO}" TITLE="chore(release): prepare for ${VERSION}" # Read CHANGES.md and escape for JSON DESCRIPTION=$(cat CHANGES.md | jq -Rs .) DESCRIPTION="${DESCRIPTION:1:-1}" # Remove surrounding quotes from jq # Add squash merge reminder DESCRIPTION="${DESCRIPTION} --- **Note:** Please use **Squash Merge** when merging this PR." echo "Checking for existing release PRs..." PRS=$(curl -sf \ -H "Authorization: token ${TOKEN}" \ "${API_URL}/pulls?state=open" | jq '[.[] | select(.head.ref == "next-release")]') PR_INDEX=$(echo "${PRS}" | jq -r '.[0].number // empty') echo "Checking for existing next-release branch..." BRANCH_EXISTS=$(curl -sf \ -H "Authorization: token ${TOKEN}" \ "${API_URL}/branches/next-release" 2>/dev/null && echo "true" || echo "false") # Prepare CHANGELOG.md content CHANGELOG_CONTENT=$(base64 -w0 < CHANGELOG.md) # Prepare .version content VERSION_JSON=$(jq -n --arg v "${VERSION}" '{"version":$v}') VERSION_CONTENT=$(echo "${VERSION_JSON}" | base64 -w0) if [ "${BRANCH_EXISTS}" = "true" ]; then echo "Updating existing next-release branch..." # Get SHA of existing CHANGELOG.md CHANGELOG_SHA=$(curl -sf \ -H "Authorization: token ${TOKEN}" \ "${API_URL}/contents/CHANGELOG.md?ref=next-release" | jq -r '.sha // empty') # Update or create CHANGELOG.md if [ -n "${CHANGELOG_SHA}" ]; then curl -sf -X PUT \ -H "Authorization: token ${TOKEN}" \ -H "Content-Type: application/json" \ --data "$(jq -n \ --arg content "${CHANGELOG_CONTENT}" \ --arg sha "${CHANGELOG_SHA}" \ --arg message "${TITLE}" \ --arg branch "next-release" \ '{content: $content, sha: $sha, message: $message, branch: $branch}')" \ "${API_URL}/contents/CHANGELOG.md" else curl -sf -X POST \ -H "Authorization: token ${TOKEN}" \ -H "Content-Type: application/json" \ --data "$(jq -n \ --arg content "${CHANGELOG_CONTENT}" \ --arg message "${TITLE}" \ --arg branch "next-release" \ '{content: $content, message: $message, branch: $branch, new_branch: $branch}')" \ "${API_URL}/contents/CHANGELOG.md" fi # Get SHA of existing .version VERSION_SHA=$(curl -sf \ -H "Authorization: token ${TOKEN}" \ "${API_URL}/contents/.version?ref=next-release" | jq -r '.sha // empty') # Update or create .version if [ -n "${VERSION_SHA}" ]; then curl -sf -X PUT \ -H "Authorization: token ${TOKEN}" \ -H "Content-Type: application/json" \ --data "$(jq -n \ --arg content "${VERSION_CONTENT}" \ --arg sha "${VERSION_SHA}" \ --arg message "${TITLE}" \ --arg branch "next-release" \ '{content: $content, sha: $sha, message: $message, branch: $branch}')" \ "${API_URL}/contents/.version" else curl -sf -X POST \ -H "Authorization: token ${TOKEN}" \ -H "Content-Type: application/json" \ --data "$(jq -n \ --arg content "${VERSION_CONTENT}" \ --arg message "${TITLE}" \ --arg branch "next-release" \ '{content: $content, message: $message, branch: $branch}')" \ "${API_URL}/contents/.version" fi else echo "Creating new next-release branch with CHANGELOG.md..." curl -sf -X POST \ -H "Authorization: token ${TOKEN}" \ -H "Content-Type: application/json" \ --data "$(jq -n \ --arg content "${CHANGELOG_CONTENT}" \ --arg message "${TITLE}" \ --arg branch "next-release" \ --arg new_branch "next-release" \ '{content: $content, message: $message, branch: $branch, new_branch: $new_branch}')" \ "${API_URL}/contents/CHANGELOG.md" echo "Adding .version to next-release branch..." curl -sf -X POST \ -H "Authorization: token ${TOKEN}" \ -H "Content-Type: application/json" \ --data "$(jq -n \ --arg content "${VERSION_CONTENT}" \ --arg message "${TITLE}" \ --arg branch "next-release" \ '{content: $content, message: $message, branch: $branch}')" \ "${API_URL}/contents/.version" fi if [ -n "${PR_INDEX}" ]; then echo "Updating existing PR #${PR_INDEX}..." curl -sf -X PATCH \ -H "Authorization: token ${TOKEN}" \ -H "Content-Type: application/json" \ --data "$(jq -n \ --arg title "${TITLE}" \ --arg body "${DESCRIPTION}" \ '{title: $title, body: $body}')" \ "${API_URL}/pulls/${PR_INDEX}" else echo "Creating new PR..." curl -sf -X POST \ -H "Authorization: token ${TOKEN}" \ -H "Content-Type: application/json" \ --data "$(jq -n \ --arg title "${TITLE}" \ --arg body "${DESCRIPTION}" \ --arg head "next-release" \ --arg base "${DEFAULT_BRANCH}" \ '{title: $title, body: $body, head: $head, base: $base}')" \ "${API_URL}/pulls" fi prepare-release: name: Prepare Release runs-on: ubuntu-latest needs: preconditions if: | (github.ref_type == 'branch' && github.ref_name == github.event.repository.default_branch) || github.ref_type == 'tag' container: image: orhunp/git-cliff:2.10.1@sha256:6ba0d1fcb051bd7b154cfb19c4b2b3bfa2c22c475f5285fc30606777b6573119 outputs: version: ${{ steps.version.outputs.version }} steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Generate changelog run: | if [ "${{ github.ref_type }}" = "tag" ]; then git-cliff --bump --latest --strip header > CHANGES.md else git-cliff --bump --unreleased --strip header > CHANGES.md fi - name: Get version id: version run: | VERSION=$(git-cliff --bumped-version 2>/dev/null || echo "") echo "version=${VERSION}" >> $GITHUB_OUTPUT echo "${VERSION}" > VERSION - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: release-artifacts path: | CHANGES.md VERSION create-release: name: Create Release runs-on: ubuntu-latest needs: prepare-release if: | github.ref_type == 'branch' && github.ref_name == github.event.repository.default_branch && inputs.tag_only != true container: image: amd64/alpine:3.22.2@sha256:b687e78c6e2785808446f45b52f1540a1e58adc07bdcffea354933b18c613d90 steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install dependencies run: apk add --no-cache git jq curl - name: Download artifacts uses: actions/download-artifact@v4 with: name: release-artifacts - name: Create release env: TOKEN: ${{ secrets.UNBOUND_RELEASE_TOKEN }} REPOSITORY: ${{ github.repository }} DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} run: | if [ ! -r .version ]; then echo "Version file not found" exit 0 fi VERSION=$(cat .version 2>/dev/null | jq -r '.version') LATEST=$(git describe --abbrev=0 --tags 2>/dev/null || echo '') if [ -n "${LATEST}" ] && [ "${VERSION}" = "${LATEST}" ]; then echo "Version ${VERSION} already exists" exit 0 fi OWNER=$(echo "${REPOSITORY}" | cut -d'/' -f1) REPO=$(echo "${REPOSITORY}" | cut -d'/' -f2) API_URL="${GITEA_URL}/api/v1/repos/${OWNER}/${REPO}" NAME=$(cat VERSION) MESSAGE=$(cat CHANGES.md | jq -Rs .) MESSAGE="${MESSAGE:1:-1}" # Remove surrounding quotes echo "Creating release ${NAME}..." curl -sf -X POST \ -H "Authorization: token ${TOKEN}" \ -H "Content-Type: application/json" \ --data "$(jq -n \ --arg tag_name "${NAME}" \ --arg name "${NAME}" \ --arg body "${MESSAGE}" \ --arg target "${DEFAULT_BRANCH}" \ '{tag_name: $tag_name, name: $name, body: $body, target_commitish: $target}')" \ "${API_URL}/releases" create-tag: name: Create Tag runs-on: ubuntu-latest needs: prepare-release if: | github.ref_type == 'branch' && github.ref_name == github.event.repository.default_branch && inputs.tag_only == true container: image: amd64/alpine:3.22.2@sha256:b687e78c6e2785808446f45b52f1540a1e58adc07bdcffea354933b18c613d90 steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install dependencies run: apk add --no-cache git jq curl - name: Download artifacts uses: actions/download-artifact@v4 with: name: release-artifacts - name: Create tag env: TOKEN: ${{ secrets.UNBOUND_RELEASE_TOKEN }} REPOSITORY: ${{ github.repository }} DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} run: | if [ ! -r .version ]; then echo "Version file not found" exit 0 fi VERSION=$(cat .version 2>/dev/null | jq -r '.version') LATEST=$(git describe --abbrev=0 --tags 2>/dev/null || echo '') if [ -n "${LATEST}" ] && [ "${VERSION}" = "${LATEST}" ]; then echo "Version ${VERSION} already exists" exit 0 fi OWNER=$(echo "${REPOSITORY}" | cut -d'/' -f1) REPO=$(echo "${REPOSITORY}" | cut -d'/' -f2) API_URL="${GITEA_URL}/api/v1/repos/${OWNER}/${REPO}" NAME=$(cat VERSION) echo "Creating tag ${NAME}..." curl -sf -X POST \ -H "Authorization: token ${TOKEN}" \ -H "Content-Type: application/json" \ --data "$(jq -n \ --arg tag_name "${NAME}" \ --arg target "${DEFAULT_BRANCH}" \ --arg message "${NAME}" \ '{tag_name: $tag_name, target: $target, message: $message}')" \ "${API_URL}/tags"