Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 87 additions & 16 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@ name: Release
on:
push:
branches: [main]
# Recovery path. release-please only ever emits `release_created` once per
# release, so once a tag exists there is no way to re-run build and sign for
# it: "Re-run all jobs" makes release-please report nothing to do and every
# downstream job skip, while the run goes green. That is how v0.10.0 ended up
# tagged with an empty draft release and no way to finish it.
#
# Dispatch with the tag to build, sign and publish an existing release.
workflow_dispatch:
inputs:
tag:
description: 'Existing release tag to build, sign and publish (e.g. v0.10.0)'
required: true
type: string

permissions:
contents: write
Expand All @@ -13,6 +26,7 @@ env:

jobs:
release-please:
if: github.event_name == 'push'
runs-on: ubuntu-latest
outputs:
release_created: ${{ steps.release.outputs.release_created }}
Expand Down Expand Up @@ -40,12 +54,61 @@ jobs:
if: ${{ steps.release.outputs.release_created }}
env:
GH_TOKEN: ${{ github.token }}
run: gh release edit "${{ steps.release.outputs.tag_name }}" --draft=true
# `-R` is not optional: this job has no `actions/checkout`, so `gh`
# has no git remote to infer the repository from and dies with
# "fatal: not a git repository". aur-publish.yml already carries this
# exact warning; this call was added without heeding it, and the cost
# was a published, empty v0.10.0 sitting at /releases/latest.
run: gh release edit "${{ steps.release.outputs.tag_name }}" -R "${{ github.repository }}" --draft=true

# One place that answers "which tag are we building?", so the jobs below do
# not each have to know whether this run came from a merge or a dispatch.
target:
name: Resolve the target release
needs: release-please
# NOT `always()`: that would also run this - and everything after it - when
# release-please FAILED, which is the brake that stopped the v0.10.0
# incident from going further than an empty draft. `!cancelled() &&
# !failure()` still lets the job run when release-please is SKIPPED, which
# is what a workflow_dispatch does.
if: ${{ !cancelled() && !failure() && (needs.release-please.outputs.release_created || github.event_name == 'workflow_dispatch') }}
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.pick.outputs.tag }}
steps:
- id: pick
env:
GH_TOKEN: ${{ github.token }}
# release-please's tag on a merge; the dispatch input on a recovery.
# On a dispatch release-please is skipped and its outputs are empty,
# so the fallback resolves in exactly one direction each time.
FROM_RELEASE: ${{ needs.release-please.outputs.tag_name }}
FROM_DISPATCH: ${{ inputs.tag }}
run: |
tag="${FROM_RELEASE:-$FROM_DISPATCH}"
[ -n "$tag" ] || { echo "::error::no tag to build"; exit 1; }

# Refuse to touch a release that is already visible to users. The
# uploader has overwrite_files on by default, so a dispatch against a
# published tag would DELETE and replace its live binaries and only
# then hit the draft assertion - leaving published assets whose .sig
# and .meta describe bytes that no longer exist. Self-update is
# fail-closed, so that breaks every install of that version.
if ! gh release view "$tag" -R "$GITHUB_REPOSITORY" --json isDraft --jq .isDraft > /tmp/state 2>/dev/null; then
echo "::error::no release found for $tag"
exit 1
fi
if [ "$(cat /tmp/state)" != "true" ]; then
echo "::error::$tag is already published. Re-running would overwrite its live assets and leave their signatures stale. Draft it first if you really mean to replace it: gh release edit $tag --draft=true"
exit 1
fi

echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "building $tag (draft)"

build:
name: Build ${{ matrix.asset }}
needs: release-please
if: ${{ needs.release-please.outputs.release_created }}
needs: target
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
Expand All @@ -65,7 +128,13 @@ jobs:
asset: colony-macos-x86

steps:
# Check out the TAG, not the branch. On a recovery dispatch the default
# ref is the branch the workflow was dispatched from, so without this the
# rebuild would produce binaries from a different commit than the one the
# tag names - and the .meta sidecar binds those bytes to that version.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.target.outputs.tag }}

- uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable branch
with:
Expand Down Expand Up @@ -132,7 +201,7 @@ jobs:
- name: Upload binary to GitHub Release
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
with:
tag_name: ${{ needs.release-please.outputs.tag_name }}
tag_name: ${{ needs.target.outputs.tag }}
files: ${{ matrix.asset }}
draft: true

Expand All @@ -143,9 +212,9 @@ jobs:
shell: bash
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.release-please.outputs.tag_name }}
TAG: ${{ needs.target.outputs.tag }}
run: |
state=$(gh release view "$TAG" --json isDraft --jq .isDraft)
state=$(gh release view "$TAG" -R "$GITHUB_REPOSITORY" --json isDraft --jq .isDraft)
[ "$state" = "true" ] || {
echo "::error::$TAG is no longer a draft after upload - it is public without signatures. Re-draft it NOW: gh release edit $TAG --draft=true"
exit 1
Expand All @@ -159,8 +228,7 @@ jobs:
# silently again.
sign:
name: Sign release assets
needs: [release-please, build]
if: ${{ needs.release-please.outputs.release_created }}
needs: [target, build]
runs-on: ubuntu-latest
env:
# Single source of truth for the four launcher assets. Declared once so the
Expand All @@ -169,15 +237,19 @@ jobs:
# which is exactly how v0.7.0 shipped.
ASSETS: colony-linux colony-windows.exe colony-macos colony-macos-x86
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.release-please.outputs.tag_name }}
TAG: ${{ needs.target.outputs.tag }}
steps:
# Same tag as the build, so the signing script that runs is the one this
# release actually shipped with.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.target.outputs.tag }}

- name: Download release binaries
run: |
mkdir dist
for a in $ASSETS; do PATTERNS="$PATTERNS --pattern $a"; done
gh release download "$TAG" --dir dist $PATTERNS
gh release download "$TAG" -R "$GITHUB_REPOSITORY" --dir dist $PATTERNS
for a in $ASSETS; do
[ -s "dist/$a" ] || { echo "::error::release asset $a is missing or empty"; exit 1; }
done
Expand Down Expand Up @@ -219,13 +291,13 @@ jobs:
echo "all assets carry .sig + .meta + .meta.sig, correctly bound"

- name: Upload signatures and metadata
run: gh release upload "$TAG" dist/*.sig dist/*.meta --clobber
run: gh release upload "$TAG" -R "$GITHUB_REPOSITORY" dist/*.sig dist/*.meta --clobber

# ...and again against the PUBLISHED release, because a local file proves
# nothing about what users can actually download.
- name: Verify the published release carries every asset
run: |
gh release view "$TAG" --json assets --jq '.assets[].name' | sort > /tmp/published
gh release view "$TAG" -R "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' | sort > /tmp/published
for a in $ASSETS; do
for required in "$a" "$a.sig" "$a.meta" "$a.meta.sig"; do
grep -qx "$required" /tmp/published \
Expand All @@ -239,7 +311,7 @@ jobs:
# update it cannot apply, and the README's download link never points at
# an empty release.
- name: Publish the release
run: gh release edit "$TAG" --draft=false
run: gh release edit "$TAG" -R "$GITHUB_REPOSITORY" --draft=false

# Chained here (not on `release: published`) because release-please creates
# the release with the default GITHUB_TOKEN, whose events do not trigger
Expand All @@ -248,11 +320,10 @@ jobs:
# the AUR job hashes them.
aur:
name: Publish colony-bin to AUR
needs: [release-please, sign]
if: ${{ needs.release-please.outputs.release_created }}
needs: [target, sign]
uses: ./.github/workflows/aur-publish.yml
with:
tag: ${{ needs.release-please.outputs.tag_name }}
tag: ${{ needs.target.outputs.tag }}
# NOT `inherit`: the AUR job needs one secret, and inheriting handed it
# the release signing key as well.
secrets:
Expand Down
Loading