Skip to content

docs(versioning): document major-date-patch release policy #33

docs(versioning): document major-date-patch release policy

docs(versioning): document major-date-patch release policy #33

Workflow file for this run

name: release
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Release tag (e.g. v1.260424.0). Optional."
required: false
type: string
permissions:
contents: write
jobs:
build-and-release:
runs-on: macos-14
env:
RELEASE_ASSET_NAME: kmsg-macos-universal
TAP_REPO: channprj/homebrew-tap
TAP_REPO_REF: main
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Swift 6.0
uses: swift-actions/setup-swift@v2
with:
swift-version: "6.0"
- name: Resolve release tag
id: meta
shell: bash
run: |
set -euo pipefail
TAG=""
SOURCE=""
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
INPUT_TAG='${{ inputs.tag }}'
INPUT_TAG="$(printf '%s' "$INPUT_TAG" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
if [[ -n "$INPUT_TAG" ]]; then
TAG="$INPUT_TAG"
SOURCE="dispatch_input"
else
RAW_VERSION="$(sed -n '1p' VERSION | tr -d '\r')"
RAW_VERSION="$(printf '%s' "$RAW_VERSION" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
if [[ -z "$RAW_VERSION" ]]; then
echo "Unable to resolve release tag from VERSION file"
exit 1
fi
TAG="v$RAW_VERSION"
SOURCE="version_file_fallback"
fi
else
TAG="${GITHUB_REF_NAME}"
SOURCE="push_ref"
fi
if [[ -z "$TAG" ]]; then
echo "Unable to resolve release tag"
exit 1
fi
export TAG
python3 <<'PY'
import datetime as dt
import os
import re
import sys
tag = os.environ["TAG"]
match = re.fullmatch(r"v(\d+)\.(\d{2})(\d{2})(\d{2})\.(\d+)", tag)
if match is None:
print(f"Invalid tag '{tag}'. Expected format: vMAJOR.YYMMDD.PATCH_COUNT")
raise SystemExit(1)
major, year_suffix, month, day, patch_count = (int(part) for part in match.groups())
if major < 1:
print(f"Invalid tag '{tag}'. MAJOR must be >= 1")
raise SystemExit(1)
if patch_count < 0:
print(f"Invalid tag '{tag}'. PATCH_COUNT must be >= 0")
raise SystemExit(1)
try:
dt.date(2000 + year_suffix, month, day)
except ValueError:
print(f"Invalid tag '{tag}'. YYMMDD must resolve to a real calendar date")
raise SystemExit(1)
PY
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "source=$SOURCE" >> "$GITHUB_OUTPUT"
echo "Resolved tag: $TAG"
echo "Resolved tag source: $SOURCE"
- name: Build universal binary
shell: bash
run: |
set -euo pipefail
swift --version
swift build -c release --arch arm64
swift build -c release --arch x86_64
ARM_BIN_DIR="$(swift build -c release --arch arm64 --show-bin-path)"
X86_BIN_DIR="$(swift build -c release --arch x86_64 --show-bin-path)"
ARM_BIN="$ARM_BIN_DIR/kmsg"
X86_BIN="$X86_BIN_DIR/kmsg"
test -f "$ARM_BIN"
test -f "$X86_BIN"
lipo -create -output "$RELEASE_ASSET_NAME" "$ARM_BIN" "$X86_BIN"
chmod +x "$RELEASE_ASSET_NAME"
file "$RELEASE_ASSET_NAME"
lipo -info "$RELEASE_ASSET_NAME"
- name: Create or update release
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.meta.outputs.tag }}
shell: bash
run: |
set -euo pipefail
if gh release view "$TAG" > /dev/null 2>&1; then
gh release upload "$TAG" "$RELEASE_ASSET_NAME" --clobber
else
gh release create "$TAG" "$RELEASE_ASSET_NAME" --title "$TAG" --generate-notes --target "$GITHUB_SHA"
fi
- name: Check Homebrew tap sync configuration
id: tap-sync
shell: bash
run: |
set -euo pipefail
{
echo "## Homebrew tap sync"
echo "- Repository: $TAP_REPO"
echo "- Branch: $TAP_REPO_REF"
echo "- Policy: required"
} >> "$GITHUB_STEP_SUMMARY"
if [[ -z "${{ secrets.TAP_REPO_TOKEN }}" ]]; then
echo "::error::TAP_REPO_TOKEN is required for Homebrew tap sync."
{
echo "- Status: failed"
echo "- Reason: TAP_REPO_TOKEN is missing"
} >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
echo "enabled=true" >> "$GITHUB_OUTPUT"
- name: Checkout tap repository
if: steps.tap-sync.outputs.enabled == 'true'
uses: actions/checkout@v4
with:
repository: ${{ env.TAP_REPO }}
token: ${{ secrets.TAP_REPO_TOKEN }}
ref: ${{ env.TAP_REPO_REF }}
path: tap-repo
- name: Build Homebrew release metadata
if: steps.tap-sync.outputs.enabled == 'true'
env:
GH_TOKEN: ${{ github.token }}
TAG_FILE: ${{ runner.temp }}/kmsg-homebrew-meta/tags.txt
JSON_FILE: ${{ runner.temp }}/kmsg-homebrew-meta/releases.json
DOWNLOAD_DIR: ${{ runner.temp }}/kmsg-homebrew-meta/downloads
shell: bash
run: |
set -euo pipefail
TAG="${{ steps.meta.outputs.tag }}"
KEEP_VERSIONS=10
META_DIR="$RUNNER_TEMP/kmsg-homebrew-meta"
TAG_FILE="$META_DIR/tags.txt"
JSON_FILE="$META_DIR/releases.json"
DOWNLOAD_DIR="$META_DIR/downloads"
mkdir -p "$META_DIR" "$DOWNLOAD_DIR"
python3 <<'PY' > "$TAG_FILE"
import datetime as dt
import re
import subprocess
KEEP_VERSIONS = 10
major_date_patch_pattern = re.compile(r"^v(\d+)\.(\d{2})(\d{2})(\d{2})\.(\d+)$")
legacy_pattern = re.compile(r"^v(\d+)\.(\d+)\.(\d+)$")
def parse_release_tag(tag: str):
major_date_patch_match = major_date_patch_pattern.fullmatch(tag)
if major_date_patch_match is not None:
major, year_suffix, month, day, patch_count = (int(part) for part in major_date_patch_match.groups())
try:
dt.date(2000 + year_suffix, month, day)
except ValueError:
return None
if major < 1 or patch_count < 0:
return None
return (2, major, 2000 + year_suffix, month, day, patch_count)
legacy_match = legacy_pattern.fullmatch(tag)
if legacy_match is not None:
major, minor, patch = (int(part) for part in legacy_match.groups())
return (1, major, minor, patch, 0)
return None
tags = subprocess.check_output(["git", "tag", "--list"], text=True).splitlines()
release_tags = [tag for tag in tags if parse_release_tag(tag) is not None]
release_tags.sort(key=parse_release_tag, reverse=True)
for tag in release_tags[:KEEP_VERSIONS]:
print(tag)
PY
while IFS= read -r RELEASE_TAG; do
[[ -n "$RELEASE_TAG" ]] || continue
if [[ "$RELEASE_TAG" == "$TAG" ]]; then
cp "$RELEASE_ASSET_NAME" "$DOWNLOAD_DIR/$RELEASE_TAG-$RELEASE_ASSET_NAME"
else
gh release download "$RELEASE_TAG" \
--repo "$GITHUB_REPOSITORY" \
--pattern "$RELEASE_ASSET_NAME" \
--dir "$DOWNLOAD_DIR/$RELEASE_TAG"
mv "$DOWNLOAD_DIR/$RELEASE_TAG/$RELEASE_ASSET_NAME" "$DOWNLOAD_DIR/$RELEASE_TAG-$RELEASE_ASSET_NAME"
rmdir "$DOWNLOAD_DIR/$RELEASE_TAG"
fi
done < "$TAG_FILE"
python3 <<'PY'
import hashlib
import json
import os
from pathlib import Path
repository = os.environ["GITHUB_REPOSITORY"]
asset_name = os.environ["RELEASE_ASSET_NAME"]
tag_file = Path(os.environ["TAG_FILE"])
download_dir = Path(os.environ["DOWNLOAD_DIR"])
json_file = Path(os.environ["JSON_FILE"])
metadata = {}
for tag in tag_file.read_text(encoding="utf-8").splitlines():
if not tag:
continue
version = tag.removeprefix("v")
asset_path = download_dir / f"{tag}-{asset_name}"
sha256 = hashlib.sha256(asset_path.read_bytes()).hexdigest()
metadata[tag] = {
"url": f"https://github.com/{repository}/releases/download/{tag}/{asset_name}",
"sha256": sha256,
"version": version,
}
json_file.write_text(json.dumps(metadata, indent=2, sort_keys=True), encoding="utf-8")
PY
- name: Update Homebrew formulas
if: steps.tap-sync.outputs.enabled == 'true'
shell: bash
run: |
set -euo pipefail
TAP_DIR="$GITHUB_WORKSPACE/tap-repo"
TAG="${{ steps.meta.outputs.tag }}"
python3 tools/sync_homebrew_tap.py \
--tap-dir "$TAP_DIR" \
--asset-path "$GITHUB_WORKSPACE/$RELEASE_ASSET_NAME" \
--repository "$GITHUB_REPOSITORY" \
--current-tag "$TAG" \
--release-metadata-file "$RUNNER_TEMP/kmsg-homebrew-meta/releases.json" \
--keep-versions 10
cd "$TAP_DIR"
if git diff --quiet -- README.md Formula; then
echo "Homebrew tap already up-to-date."
{
echo "- Status: success"
echo "- Result: tap repository already up-to-date for $TAG"
} >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add README.md Formula
git commit -m "chore(tap): sync kmsg formulas for $TAG"
git push origin HEAD:"$TAP_REPO_REF"
{
echo "- Status: success"
echo "- Result: pushed tap updates for $TAG"
} >> "$GITHUB_STEP_SUMMARY"