Skip to content
Merged
Show file tree
Hide file tree
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
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,23 @@ jobs:
- name: Build
run: cargo build --all-targets

# Same check as the release workflow's Windows leg: SignPath signs only
# an .exe whose version resource names the project and carries its
# version, and build.rs is what writes it. Checked here so a pull
# request that breaks it fails now rather than at the next release.
- name: Check the Windows version resource
if: runner.os == 'Windows'
shell: pwsh
run: |
$version = (cargo metadata --no-deps --format-version 1 | ConvertFrom-Json).packages |
Where-Object name -eq 'grape' | ForEach-Object version
$info = (Get-Item target\debug\grape.exe).VersionInfo
"ProductName='$($info.ProductName)' ProductVersion='$($info.ProductVersion)' FileVersion='$($info.FileVersion)'"
if ($info.ProductName -cne 'Grape' -or $info.ProductVersion -cne $version) {
"::error::grape.exe has ProductName '$($info.ProductName)' and ProductVersion '$($info.ProductVersion)', expected 'Grape' and '$version'"
exit 1
}

- name: Test
run: cargo test
# 19 of the player tests need a real audio device and are #[ignore]d, so
Expand Down
229 changes: 107 additions & 122 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,29 +1,42 @@
# Colony Rust program — release workflow
#
# Copy to your repo as .github/workflows/release.yml and replace grape
# with your binary name (lowercase, as it appears in Cargo.toml).
# Grape - release workflow
#
# Built from templates/sign-and-publish-caller.yml in Project-Colony-Resources.
# Assets follow the Colony naming convention, so Colony auto-detects which
# platforms you ship and your colony.json only needs a name and a category:
# platforms Grape ships:
#
# grape-linux grape-windows.exe
# grape-macos grape-macos-x86
#
# See design/releases.md in Project-Colony-Resources for the full contract.
# The build legs only build, check and upload artifacts; they never see a key.
# Authenticode (SignPath, once approved), the ed25519 .sig/.meta/.meta.sig,
# verification and publishing all happen in
# .github/workflows/sign-and-publish.yml of Project-Colony-Resources, in this
# same run. See design/releases.md there, section "Shared signing workflow".
#
# Action refs are pinned to commit SHAs on purpose: this workflow handles the
# organisation's release signing key, and whoever can move a floating tag can
# read that secret.
# Action refs are pinned to commit SHAs on purpose: the called workflow handles
# the organisation's release signing key, and whoever can move a floating tag
# can read that secret.

name: Release

on:
push:
branches: [main]

permissions:
contents: write
pull-requests: write
# Recovery path: finish an existing DRAFT release whose run failed.
# release-please emits `release_created` once per release, so "Re-run all
# jobs" on the original run makes it report nothing to do and every job
# below skip. Start the dispatch from the tag ("Use workflow from: v0.4.1",
# or `gh workflow run release.yml --ref v0.4.1 -f tag=v0.4.1`): with SignPath
# on, sign-and-publish refuses a run whose commit is not the tag's, since
# SignPath records the run's commit as the source of what it signs.
workflow_dispatch:
inputs:
tag:
description: "Existing draft release to build, sign and publish (e.g. v0.4.1). Run from that same tag."
required: true
type: string

permissions: {}

concurrency:
group: release-${{ github.ref }}
Expand All @@ -36,44 +49,54 @@ jobs:
# Assembles a release PR from the conventional commits since the last release.
# Merging that PR is what creates the tag; everything below runs only then.
release-please:
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
steps:
# No `release-type` input, on purpose: when it is set, the action
# ignores both files below, and with them the changelog sections, the
# bump policy and the version in the manifest. The release type lives in
# release-please-config.json.
- uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
id: release
with:
release-type: rust
# Named explicitly rather than left to the action's defaults: the
# config is in manifest mode, and a silent fallback to simple mode
# would version the repo differently without saying so.
config-file: release-please-config.json
manifest-file: .release-please-manifest.json

# release-please PUBLISHES the release - and moves /releases/latest -
# before a single binary exists. For the whole build window every Colony
# in the field would show an update badge whose every click fails: a
# missing asset first, then a fail-closed signature refusal once the
# binary is up but its .sig is not. Held as a draft here and published by
# the `publish` job once everything is verified present.
# in the field would show an update badge whose every click fails. Held
# as a draft here; sign-and-publish refuses anything else and publishes
# it last, once every file is signed and verified on the release.
- name: Hold the release as a draft until it is complete
if: ${{ steps.release.outputs.release_created }}
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.release.outputs.tag_name }}
# `-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". That is exactly what happened on v0.3.0 — this step
# failed, `build` and `publish` were skipped, and the tag was published
# with no assets at all. Colony carries the same warning after paying
# for it with an empty v0.10.0.
run: gh release edit "${{ steps.release.outputs.tag_name }}" -R "${{ github.repository }}" --draft=true
# a git repository". That is exactly what happened on v0.3.0: this step
# failed, the build was skipped, and the tag was published with no
# assets at all.
run: gh release edit "$TAG" -R "$GITHUB_REPOSITORY" --draft=true

build:
name: build ${{ matrix.asset }}
needs: release-please
if: ${{ needs.release-please.outputs.release_created }}
# Runs on a merge that created a release, or on a recovery dispatch (when
# release-please is skipped). NOT `always()`: a FAILED release-please must
# stop the run at an empty draft.
if: ${{ !cancelled() && !failure() && (needs.release-please.outputs.release_created || github.event_name == 'workflow_dispatch') }}
# GitHub-hosted runners only: SignPath rejects a signing request if any
# job leading to it ran on a self-hosted runner.
runs-on: ${{ matrix.os }}
permissions:
contents: read
strategy:
fail-fast: false
matrix:
Expand All @@ -94,15 +117,19 @@ jobs:
asset: "grape-macos-x86"

steps:
# The tag, not the branch: a recovery dispatch must rebuild the tagged
# commit, since the .meta sidecar binds these bytes to that version.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.release-please.outputs.tag_name || inputs.tag }}
persist-credentials: false

- uses: dtolnay/rust-toolchain@89b12181fb390509a0842a86cc55eeb8eb928c1d # stable branch
with:
targets: ${{ matrix.target }}

- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: ${{ matrix.target }}
# No build cache here: SignPath forbids release builds that reuse
# outputs of earlier, unverified builds.

# alsa-sys is the only crate needing a system development package on
# Linux; it resolves libasound through pkg-config. aws-lc-sys also links
Expand Down Expand Up @@ -131,15 +158,10 @@ jobs:
# A manifest can PARSE perfectly and still leave the program listed in
# Colony with no Download button, because none of its assets match a
# convention. `colony validate-manifest` given the asset names catches
# exactly that. Linux only: one check per release is enough.
#
# Deliberately ordered BEFORE the signing step: this downloads and runs a
# binary, and the signing key must never have touched this runner's disk
# while a fetched executable is running on it.
# exactly that. Linux only: one check per release is enough. No key is
# on this runner: signing happens in sign-and-publish.
- name: Validate colony.json
if: runner.os == 'Linux'
env:
GH_TOKEN: ${{ github.token }}
run: |
curl -fsSL -o colony-validator \
https://github.com/Project-Colony/Colony/releases/latest/download/colony-linux
Expand All @@ -159,6 +181,24 @@ jobs:
chmod +x "${{ matrix.asset }}" || true
"./${{ matrix.asset }}" --version

# SignPath signs only an .exe whose version resource names the project
# and carries its version (build.rs writes it). A build that lost either
# would be refused at signing time, hours later; this fails it here.
- name: Check the Windows version resource
if: runner.os == 'Windows'
shell: pwsh
env:
ASSET: ${{ matrix.asset }}
TAG: ${{ needs.release-please.outputs.tag_name || inputs.tag }}
run: |
$info = (Get-Item $env:ASSET).VersionInfo
$version = $env:TAG -replace '^v', ''
"ProductName='$($info.ProductName)' ProductVersion='$($info.ProductVersion)' FileVersion='$($info.FileVersion)'"
if ($info.ProductName -cne 'Grape' -or $info.ProductVersion -cne $version) {
"::error::$env:ASSET has ProductName '$($info.ProductName)' and ProductVersion '$($info.ProductVersion)', expected 'Grape' and '$version'"
exit 1
}

# The Intel macOS artefact is cross-compiled on an arm64 runner and
# cannot be executed there, so assert its architecture instead - that
# being the leg with the fewest users, where a regression survives
Expand All @@ -170,90 +210,35 @@ jobs:
file "${{ matrix.asset }}" | grep -q 'x86_64' \
|| { echo "::error::${{ matrix.asset }} is not an x86_64 binary"; exit 1; }

# Signing is opt-in. Delete this step and the next unless colony.json sets
# "signed": true — a program that advertises signatures but ships none
# fails closed and cannot be installed at all.
#
# Requires the COLONY_SIGNING_KEY_PEM organisation secret (the ed25519
# private key, PEM). The key never lives in a repository.
#
# `shell: bash` rather than skipping Windows: the Windows runner has bash
# and openssl, and an unsigned .exe in a repo that declares
# "signed": true is refused at install time — so excluding it did not
# produce an unsigned-but-working asset, it produced an uninstallable one.
- name: Sign the artifact
shell: bash
env:
SIGNING_KEY: ${{ secrets.COLONY_SIGNING_KEY_PEM }}
run: |
if [ -z "$SIGNING_KEY" ]; then
echo "::error::COLONY_SIGNING_KEY_PEM is not set but signing is enabled"
exit 1
fi
keyfile="$(mktemp)"
trap 'rm -f "$keyfile"' EXIT
printf '%s' "$SIGNING_KEY" > "$keyfile"
COLONY_SIGNING_KEY="$keyfile" \
COLONY_RELEASE_VERSION="${{ needs.release-please.outputs.tag_name }}" \
./scripts/sign-release.sh "${{ matrix.asset }}"

- name: Upload to the GitHub release
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
# The file at the artifact root, under its release name. The name
# prefix is what `artifact-pattern` below matches.
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
# Without this the uploader PUBLISHES the release it is uploading to,
# because `draft` defaults to false and the action updates the release
# rather than only adding files. The first platform to finish
# therefore undoes the draft-hold, and the remaining platforms upload
# into a release that is already at /releases/latest. v0.3.1 went out
# that way: published with Linux and macOS while Windows was still
# compiling. `publish` is what makes it visible, once it has verified
# every asset is present.
draft: true
tag_name: ${{ needs.release-please.outputs.tag_name }}
files: |
${{ matrix.asset }}
${{ matrix.asset }}.sig
${{ matrix.asset }}.meta
${{ matrix.asset }}.meta.sig
# true: a signature that silently failed to upload is exactly the
# state that makes the release uninstallable, so it must fail here
# rather than at the user's machine.
fail_on_unmatched_files: true

# Nothing is visible to users until every asset and every signature is really
# on the release. Chained as `needs:` rather than `on: release published`,
# because a release created by release-please with the default GITHUB_TOKEN
# does not fire release events - a workflow written that way never runs, and
# never says so.
publish:
name: Publish the release
name: build-${{ matrix.asset }}
path: ${{ matrix.asset }}
if-no-files-found: error
retention-days: 1

# Authenticode (once SignPath is on), .sig/.meta/.meta.sig over the final
# bytes, upload to the draft, download again, verify, publish. Chained as
# `needs:` in this same run: SignPath accepts only an artifact uploaded by
# the run that requests the signature.
sign-and-publish:
needs: [release-please, build]
if: ${{ needs.release-please.outputs.release_created }}
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.release-please.outputs.tag_name }}
# Single source of truth for the asset list. Repeating it per step is how
# a platform silently ends up unsigned.
ASSETS: "grape-linux grape-windows.exe grape-macos grape-macos-x86"
# Set to false if colony.json does not declare "signed": true.
SIGNED: "true"
steps:
- name: Verify the release carries everything
run: |
gh release view "$TAG" -R "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' | sort > /tmp/published
for a in $ASSETS; do
required="$a"
if [ "$SIGNED" = "true" ]; then
required="$a $a.sig $a.meta $a.meta.sig"
fi
for f in $required; do
grep -qx "$f" /tmp/published \
|| { echo "::error::$f is missing from $TAG - Colony would refuse this release"; exit 1; }
done
done
echo "release verified"

- name: Make the release visible
# Same reason as the draft step above: no checkout in this job either.
run: gh release edit "$TAG" -R "$GITHUB_REPOSITORY" --draft=false
if: ${{ !cancelled() && needs.build.result == 'success' }}
# The ceiling for every job in the called workflow.
permissions:
actions: read
contents: write
uses: Project-Colony/Project-Colony-Resources/.github/workflows/sign-and-publish.yml@619460ff4dc0049f129955f0988d368427b9eedb # main
with:
tag: ${{ needs.release-please.outputs.tag_name || inputs.tag }}
assets: "grape-linux grape-windows.exe grape-macos grape-macos-x86"
artifact-pattern: build-*
# Uncomment once SignPath has approved this project and its project
# exists in the Project-Colony SignPath organisation:
# signpath-project-slug: "grape"
# Named, not `inherit`: the called workflow gets these two and nothing else.
secrets:
COLONY_SIGNING_KEY_PEM: ${{ secrets.COLONY_SIGNING_KEY_PEM }}
SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }}
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ zbus = "5.19.0"
[target.'cfg(target_os = "windows")'.dependencies]
windows = { version = "0.62.2", features = ["Foundation", "Media", "Storage", "Storage_Streams", "Win32_Foundation", "Win32_System_WinRT"] }

# The Windows version resource, written by build.rs. Not target-specific: a
# build dependency's cfg is the host's, and a Linux host building for Windows
# needs it too. No default features: they only add a TOML parser for reading
# [package.metadata.winresource], which build.rs does not use.
[build-dependencies]
winresource = { version = "0.1.31", default-features = false }

# mockall, pretty_assertions, tokio-test and serial_test were declared here but
# referenced nowhere in src/ or tests/. serial_test 4.0.1 also carried a 1.93.1
# rust-version, which would have put the test floor three releases above the
Expand Down
Loading
Loading