fix(release): resolve package tags once
This commit is contained in:
@@ -4,12 +4,61 @@ on:
|
|||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
release_tag:
|
||||||
|
description: Published release tag to update packages for
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
resolve-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
outputs:
|
||||||
|
tag: ${{ steps.release.outputs.tag }}
|
||||||
|
steps:
|
||||||
|
- name: Resolve published release tag
|
||||||
|
id: release
|
||||||
|
env:
|
||||||
|
EVENT_TAG: ${{ github.event.release.tag_name }}
|
||||||
|
INPUT_TAG: ${{ inputs.release_tag }}
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ "$GITHUB_EVENT_NAME" = "release" ]; then
|
||||||
|
REQUESTED_TAG="$EVENT_TAG"
|
||||||
|
else
|
||||||
|
REQUESTED_TAG="$INPUT_TAG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$REQUESTED_TAG" ]; then
|
||||||
|
echo "A release tag is required." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
RELEASE=$(gh release view "$REQUESTED_TAG" --repo "$GITHUB_REPOSITORY" \
|
||||||
|
--json tagName,isDraft,publishedAt)
|
||||||
|
RESOLVED_TAG=$(jq -r '.tagName' <<< "$RELEASE")
|
||||||
|
IS_DRAFT=$(jq -r '.isDraft' <<< "$RELEASE")
|
||||||
|
PUBLISHED_AT=$(jq -r '.publishedAt // empty' <<< "$RELEASE")
|
||||||
|
|
||||||
|
if [ "$RESOLVED_TAG" != "$REQUESTED_TAG" ] || \
|
||||||
|
[ "$IS_DRAFT" != "false" ] || [ -z "$PUBLISHED_AT" ]; then
|
||||||
|
echo "Tag '$REQUESTED_TAG' is not an exact published release tag." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "tag=$RESOLVED_TAG" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
update-homebrew:
|
update-homebrew:
|
||||||
|
needs: resolve-release
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
env:
|
||||||
|
RELEASE_TAG: ${{ needs.resolve-release.outputs.tag }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@@ -19,16 +68,14 @@ jobs:
|
|||||||
- name: Download and hash release
|
- name: Download and hash release
|
||||||
id: release
|
id: release
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ github.event.release.tag_name || github.ref_name }}"
|
|
||||||
curl --fail-with-body -L -o plezy-macos.dmg \
|
curl --fail-with-body -L -o plezy-macos.dmg \
|
||||||
"https://github.com/edde746/plezy/releases/download/$VERSION/plezy-macos.dmg"
|
"https://github.com/edde746/plezy/releases/download/$RELEASE_TAG/plezy-macos.dmg"
|
||||||
SHA256=$(shasum -a 256 plezy-macos.dmg | cut -d' ' -f1)
|
SHA256=$(shasum -a 256 plezy-macos.dmg | cut -d' ' -f1)
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "sha256=$SHA256" >> "$GITHUB_OUTPUT"
|
||||||
echo "sha256=$SHA256" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Update cask
|
- name: Update cask
|
||||||
run: |
|
run: |
|
||||||
sed -i '' "s/version \".*\"/version \"${{ steps.release.outputs.version }}\"/" Casks/plezy.rb
|
sed -i '' "s/version \".*\"/version \"$RELEASE_TAG\"/" Casks/plezy.rb
|
||||||
sed -i '' "s/sha256 \".*\"/sha256 \"${{ steps.release.outputs.sha256 }}\"/" Casks/plezy.rb
|
sed -i '' "s/sha256 \".*\"/sha256 \"${{ steps.release.outputs.sha256 }}\"/" Casks/plezy.rb
|
||||||
|
|
||||||
- name: Commit
|
- name: Commit
|
||||||
@@ -38,41 +85,45 @@ jobs:
|
|||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
git add Casks/plezy.rb
|
git add Casks/plezy.rb
|
||||||
if ! git diff --staged --quiet; then
|
if ! git diff --staged --quiet; then
|
||||||
git commit -m "chore: update cask to ${{ steps.release.outputs.version }}"
|
git commit -m "chore: update cask to $RELEASE_TAG"
|
||||||
git push origin "HEAD:$BRANCH"
|
git push origin "HEAD:$BRANCH"
|
||||||
else
|
else
|
||||||
echo "No cask changes to commit."
|
echo "No cask changes to commit."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
update-winget:
|
update-winget:
|
||||||
|
needs: resolve-release
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: vedantmgoyal9/winget-releaser@v2
|
- uses: vedantmgoyal9/winget-releaser@v2
|
||||||
with:
|
with:
|
||||||
identifier: edde746.Plezy
|
identifier: edde746.Plezy
|
||||||
installers-regex: 'plezy-windows-installer\.exe$'
|
installers-regex: 'plezy-windows-installer\.exe$'
|
||||||
|
release-tag: ${{ needs.resolve-release.outputs.tag }}
|
||||||
token: ${{ secrets.WINGET_TOKEN }}
|
token: ${{ secrets.WINGET_TOKEN }}
|
||||||
|
|
||||||
update-appcast-branch:
|
update-appcast-branch:
|
||||||
|
needs: resolve-release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
env:
|
||||||
|
RELEASE_TAG: ${{ needs.resolve-release.outputs.tag }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Download appcast.xml from release
|
- name: Download appcast.xml from release
|
||||||
run: |
|
run: |
|
||||||
gh release download "${{ github.event.release.tag_name }}" \
|
gh release download "$RELEASE_TAG" \
|
||||||
--pattern "appcast.xml"
|
--pattern "appcast.xml"
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Push to appcast branch
|
- name: Push to appcast branch
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ github.event.release.tag_name }}"
|
|
||||||
git config user.name "github-actions[bot]"
|
git config user.name "github-actions[bot]"
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
BLOB=$(git hash-object -w appcast.xml)
|
BLOB=$(git hash-object -w appcast.xml)
|
||||||
TREE=$(printf "100644 blob %s\tappcast.xml\n" "$BLOB" | git mktree)
|
TREE=$(printf "100644 blob %s\tappcast.xml\n" "$BLOB" | git mktree)
|
||||||
COMMIT=$(git commit-tree "$TREE" -m "Update appcast for $VERSION")
|
COMMIT=$(git commit-tree "$TREE" -m "Update appcast for $RELEASE_TAG")
|
||||||
git push origin "$COMMIT:refs/heads/appcast" --force
|
git push origin "$COMMIT:refs/heads/appcast" --force
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Guard the release-tag flow in update-packages.yml against regressions."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
WORKFLOW = Path(__file__).resolve().parents[1] / ".github/workflows/update-packages.yml"
|
||||||
|
text = WORKFLOW.read_text(encoding="utf-8")
|
||||||
|
errors: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
|
def require(condition: bool, message: str) -> None:
|
||||||
|
if not condition:
|
||||||
|
errors.append(message)
|
||||||
|
|
||||||
|
|
||||||
|
def job(name: str) -> str:
|
||||||
|
match = re.search(
|
||||||
|
rf"(?ms)^ {re.escape(name)}:\n(.*?)(?=^ [a-zA-Z0-9_-]+:\n|\Z)", text
|
||||||
|
)
|
||||||
|
require(match is not None, f"missing {name} job")
|
||||||
|
return match.group(0) if match else ""
|
||||||
|
|
||||||
|
|
||||||
|
require(
|
||||||
|
re.search(
|
||||||
|
r"(?ms)^ workflow_dispatch:\n inputs:\n release_tag:\n"
|
||||||
|
r".*? required: true\n",
|
||||||
|
text,
|
||||||
|
)
|
||||||
|
is not None,
|
||||||
|
"workflow_dispatch must require release_tag",
|
||||||
|
)
|
||||||
|
require("github.ref_name" not in text, "release tags must never fall back to a branch")
|
||||||
|
require(
|
||||||
|
text.count("github.event.release.tag_name") == 1,
|
||||||
|
"only the resolver may read the release event tag",
|
||||||
|
)
|
||||||
|
|
||||||
|
resolver = job("resolve-release")
|
||||||
|
require("gh release view \"$REQUESTED_TAG\"" in resolver, "resolver must validate the tag")
|
||||||
|
require("--json tagName,isDraft,publishedAt" in resolver, "resolver must require a published release")
|
||||||
|
require(
|
||||||
|
"tag: ${{ steps.release.outputs.tag }}" in resolver,
|
||||||
|
"resolver must expose one validated tag output",
|
||||||
|
)
|
||||||
|
|
||||||
|
homebrew = job("update-homebrew")
|
||||||
|
winget = job("update-winget")
|
||||||
|
appcast = job("update-appcast-branch")
|
||||||
|
for name, block in (
|
||||||
|
("update-homebrew", homebrew),
|
||||||
|
("update-winget", winget),
|
||||||
|
("update-appcast-branch", appcast),
|
||||||
|
):
|
||||||
|
require("needs: resolve-release" in block, f"{name} must depend on the resolver")
|
||||||
|
|
||||||
|
resolved_output = "${{ needs.resolve-release.outputs.tag }}"
|
||||||
|
require(f"RELEASE_TAG: {resolved_output}" in homebrew, "Homebrew must use the resolved tag")
|
||||||
|
require(f"release-tag: {resolved_output}" in winget, "WinGet must use the resolved tag")
|
||||||
|
require(f"RELEASE_TAG: {resolved_output}" in appcast, "appcast must use the resolved tag")
|
||||||
|
require(
|
||||||
|
'git commit -m "chore: update cask to $RELEASE_TAG"' in homebrew,
|
||||||
|
"Homebrew commit text must include the resolved tag",
|
||||||
|
)
|
||||||
|
require(
|
||||||
|
'git commit-tree "$TREE" -m "Update appcast for $RELEASE_TAG"' in appcast,
|
||||||
|
"appcast commit text must include the resolved tag",
|
||||||
|
)
|
||||||
|
require(
|
||||||
|
'gh release download "$RELEASE_TAG"' in appcast,
|
||||||
|
"appcast download must specify the resolved tag",
|
||||||
|
)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
for error in errors:
|
||||||
|
print(f"ERROR: {error}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("update-packages workflow release-tag checks passed")
|
||||||
Reference in New Issue
Block a user