docs(versioning): document major-date-patch release policy #33
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" |