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
7 changes: 3 additions & 4 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
# Release automation. release-please reads the Conventional Commit log since the last tag, works out
# the next version, and keeps one release pull request open that bumps it. Merging that pull request
# tags the release. The tag is git only: it starts no binary release (release.yml is manual,
# behind an explicit confirmation, until a binary release is decided on).
# tags the release, and that tag starts release.yml, which publishes the binaries on the GitHub
# release (owner, 2026-11-13: GitHub release only, built on a tag or on request).
#
# Versioning on 0.x: a breaking change (`feat!:`, `BREAKING CHANGE:`) bumps the minor, everything
# else bumps the patch. icon-sets-sync is versioned independently of the coupled ichava packages.
#
# Release notes stay hand-written. release-please is configured with `skip-changelog`, so it never
# writes CHANGELOG.md; the step below closes the hand-written `## [Unreleased]` section under the new
# version on the release branch; release.yml would publish that section as the release body if it is
# ever dispatched.
# version on the release branch; release.yml publishes that section as the release body.
#
# Triggers. This repository's rule is that the only `push` trigger is a tag, and release-please does
# not need one:
Expand Down
224 changes: 26 additions & 198 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,213 +1,41 @@
# Manual only. icon-sets-sync's binaries are published on the GitHub release, and
# nowhere else: no `go install` path, Homebrew tap, Scoop bucket or image (owner,
# 2026-11-13). Pushing a `vX.Y.Z` tag records the version and starts nothing; a
# release is cut when the maintainers dispatch this workflow for that version.
# Release: build icon-sets-sync's binaries and publish them on its GitHub release, and nowhere else
# (no `go install` path, Homebrew, Scoop, Winget, image or any other channel; owner, 2026-11-13).
# The pipeline is simtabi/.github's shared go-release.yml; this file only says when, and adds the
# double-click launchers (.scripts/release-launchers.sh) after GoReleaser.
#
# It runs only when dispatched against a tag with the explicit confirmation input:
# git push origin vX.Y.Z builds and publishes vX.Y.Z
# gh workflow run release.yml --repo ichava/icon-sets-sync -f tag=vX.Y.Z the same, for a tag already pushed
#
# gh workflow run release.yml --repo ichava/icon-sets-sync --ref vX.Y.Z -f confirm=release-go
#
# Any other value stops the run before anything is built or published. When it does run:
#
# - GoReleaser cross-compiles icon-sets-sync for the
# whole matrix, builds the bare binaries, the .tar.gz/.zip archives, the
# deb/rpm/apk packages and a reproducible source tarball, an SPDX SBOM per
# archive, and checksums.txt over all of it (config: src/.goreleaser.yaml);
# - cosign signs checksums.txt keylessly through Sigstore;
# - the double-click launchers (the macOS app bundle, zipped, and the Windows
# GUI launchers) are built from the same binaries and attached, with their
# own checksums-launchers.txt;
# - attest-build-provenance signs a build-provenance attestation for every
# artifact listed in either checksums file.
#
# The release body is the tagged version's CHANGELOG section, never generated
# notes alone.

# Push a version tag only for final code: tags are final, and a pushed tag publishes. The shared
# workflow also pushes the src/vX.Y.Z module tag, which matches no trigger here.
name: Release

on:
# No `push: tags` trigger: a tag is git only until a binary release is decided on.
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+-*'
workflow_dispatch:
inputs:
confirm:
description: 'Type release-go to publish binaries for the tag this run is dispatched against'
tag:
description: 'An existing vX.Y.Z tag to build and publish'
required: true
default: 'no'
type: string

permissions: {}

# One run per tag, never cancelled: two runs must not publish the same release.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
# Never cancel a release in flight. A half-published release is worse than a
# queued one.
group: release-${{ inputs.tag || github.ref_name }}
cancel-in-progress: false

permissions:
contents: read

jobs:
release:
name: Publish release
runs-on: ubuntu-latest
timeout-minutes: 30
# Our own modules come from git, never through the public proxy, so a CI
# build never feeds proxy.golang.org or sum.golang.org a tag.
env:
GOPRIVATE: github.com/ichava/*
GONOSUMDB: github.com/ichava/*
permissions:
contents: write # create the release, upload assets, push the src/ alias tag
id-token: write # Sigstore keyless identity, for cosign and the attestation
attestations: write # store the build-provenance attestation

steps:
# A release is cut only when the maintainers dispatch it for a version.
# Refuse without the explicit confirmation, and refuse on anything but a tag.
- name: Require the explicit release confirmation
env:
CONFIRM: ${{ inputs.confirm }}
REF_TYPE: ${{ github.ref_type }}
run: |
set -euo pipefail
if [ "$CONFIRM" != "release-go" ]; then
echo "::error::Dispatch with -f confirm=release-go, for a version the maintainers have decided to release."
exit 1
fi
if [ "$REF_TYPE" != "tag" ]; then
echo "::error::Dispatch this workflow against a vX.Y.Z tag (--ref vX.Y.Z)."
exit 1
fi

- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0 # GoReleaser reads the tag and the history

- name: Set up Go
uses: actions/setup-go@v7
with:
go-version-file: src/go.mod
cache-dependency-path: src/go.sum

# syft generates GoReleaser's per-archive SBOMs.
- name: Install syft
uses: anchore/sbom-action/download-syft@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c # v0.24.3

- name: Install cosign
uses: sigstore/cosign-installer@756fe2d284c7292c4e9b62c8a28f1e5629c59532 # v4.1.3

# Fail closed. A tag whose version has no CHANGELOG section is a tag on the
# wrong commit or a release nobody wrote notes for; stop rather than publish
# generated notes. Written outside the checkout: GoReleaser refuses to
# release from a dirty tree, and an untracked notes file is one.
- name: Extract the changelog section
run: |
set -euo pipefail
VERSION="${GITHUB_REF_NAME#v}"
awk -v ver="$VERSION" '
$0 ~ "^## \\[" ver "\\]" { found = 1; next }
found && /^## \[/ { exit }
found { print }
' CHANGELOG.md > "$RUNNER_TEMP/section.md"
# A pre-release tag (vX.Y.Z-rc.N) is a test build: GoReleaser marks it a
# pre-release (`prerelease: auto`) and it needs no CHANGELOG section of its
# own. A final tag still fails closed below.
if [ -z "$(tr -d '[:space:]' < "$RUNNER_TEMP/section.md")" ] && [[ "$VERSION" == *-* ]]; then
echo "Pre-release build ${GITHUB_REF_NAME}, for testing. It is not a release; the notes ship with the final v${VERSION%%-*} tag." > "$RUNNER_TEMP/section.md"
fi
if [ -z "$(tr -d '[:space:]' < "$RUNNER_TEMP/section.md")" ]; then
echo "::error::CHANGELOG.md has no '## [$VERSION]' section. Refusing to publish $GITHUB_REF_NAME."
exit 1
fi
{
cat "$RUNNER_TEMP/section.md"
echo ''
echo "**Full changelog:** https://github.com/ichava/icon-sets-sync/blob/main/CHANGELOG.md"
} > "$RUNNER_TEMP/release_notes.md"

# Go resolves this module (github.com/ichava/icon-sets-sync/src) through a
# tag named after its subdirectory, src/vX.Y.Z, and ignores vX.Y.Z. OSS
# GoReleaser rejects the prefixed tag as non-semver, so the release runs
# off vX.Y.Z and mirrors src/vX.Y.Z onto the same commit here, where it
# cannot be forgotten. src/... matches neither trigger above, so pushing
# it starts no second run.
#
# Before GoReleaser rather than after: `go install` builds from source and
# needs no assets, so the alias on a release that later failed is still
# self-consistent, while a release without its alias is not installable
# at all. A re-run finds the tag already there and leaves it alone --
# a src/ tag is permanent once the Go proxy has fetched it, so this step
# never moves one.
- name: Mirror the tag Go resolves the module through
run: |
set -euo pipefail
alias_tag="src/${GITHUB_REF_NAME}"
if git ls-remote --exit-code --tags origin "refs/tags/${alias_tag}" >/dev/null; then
echo "${alias_tag} already exists on the remote"
else
git tag "${alias_tag}" "${GITHUB_REF_NAME}^{commit}"
git push origin "refs/tags/${alias_tag}"
echo "pushed ${alias_tag}"
fi

- name: Run GoReleaser
uses: goreleaser/goreleaser-action@a4f02ab5355a589de60d917a659ce617ed19b3de # v7.3.0
with:
version: '~> v2'
args: release --clean --config src/.goreleaser.yaml --release-notes=${{ runner.temp }}/release_notes.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# Fail closed on an empty release body. v0.1.0 and v0.1.1 shipped with a
# blank body because `changelog.disable: true` made GoReleaser drop
# --release-notes; nothing in the run noticed. Read the published release
# back and require at least the changelog section we passed in.
- name: Verify the published release notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
want=$(tr -d '[:space:]' < "$RUNNER_TEMP/section.md" | wc -c)
got=$(gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --json body --jq '.body' | tr -d '[:space:]' | wc -c)
echo "section=${want} body=${got} (non-space characters)"
if [ "$got" -lt "$want" ]; then
echo "::error::The ${GITHUB_REF_NAME} release body is shorter than its CHANGELOG section (${got} < ${want})."
exit 1
fi

# GoReleaser does not build the double-click launchers: the macOS app bundle
# around the universal binary it just built, and the Windows GUI launchers
# (src/internal/tools/launchers, as `make app` and `make cross` run it). The
# .app is a folder, so it ships zipped. They get their own checksums file,
# written outside the folder so it does not list itself, and are attested
# below with everything else.
- name: Build and attach the launchers
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
out="$RUNNER_TEMP/launchers"
mkdir -p "$out"
# The published icon-sets-sync_macos_universal asset, at the build path
# GoReleaser recorded for it (flatdist reads artifacts.json the same way).
universal=$(jq -r '.[] | select(.name=="icon-sets-sync_macos_universal") | .path' .cache/goreleaser/artifacts.json)
[ -f "$universal" ] || { echo "::error::no universal binary in .cache/goreleaser/artifacts.json"; exit 1; }
go -C src run ./internal/tools/launchers \
-app "$out/icon-sets-sync.app" \
-universal "../$universal" \
-version "${GITHUB_REF_NAME#v}" \
-windows "$out"
(cd "$out" && zip -qry icon-sets-sync_macos_universal.app.zip icon-sets-sync.app && rm -rf icon-sets-sync.app)
(cd "$out" && sha256sum -- * > "$RUNNER_TEMP/checksums-launchers.txt")
mv "$RUNNER_TEMP/checksums-launchers.txt" "$out/"
ls -l "$out"
[ "$(find "$out" -name '*.exe' | wc -l)" -ge 3 ] || { echo "::error::expected at least 3 Windows launchers"; exit 1; }
gh release upload "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" "$out"/*

- name: Attest build provenance
uses: actions/attest-build-provenance@v4
with:
subject-checksums: .cache/goreleaser/checksums.txt

- name: Attest the launchers
uses: actions/attest-build-provenance@v4
with:
subject-checksums: ${{ runner.temp }}/launchers/checksums-launchers.txt
contents: write
id-token: write
attestations: write
uses: simtabi/.github/.github/workflows/go-release.yml@a1dca588d5c64798085a5fad57c872f6ce02e9aa # simtabi/.github#27
with:
tag: ${{ inputs.tag || github.ref_name }}
post-release: bash .scripts/release-launchers.sh
8 changes: 4 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5

# Nothing else reads these files until they run. `release.yml` runs only when
# dispatched against a tag, so without this a broken workflow is first observed
# as a release that refused to start -- after the decision to release was made.
# Nothing else reads these files until they run. `release.yml` runs only on a
# version tag or a dispatch naming one, so without this a broken workflow is first
# observed as a release that refused to start -- after the decision to release was made.
#
# A YAML parse is not a substitute. `yaml.safe_load` accepts a duplicate key
# and silently keeps the last one, which is how a double-applied patch shipped
Expand All @@ -198,7 +198,7 @@ jobs:
GOPRIVATE: github.com/ichava/*
GONOSUMDB: github.com/ichava/*

# release.yml only runs when dispatched against a tag, so without this the
# release.yml only runs on a version tag (or a dispatch naming one), so without this the
# first sight of a broken GoReleaser config, a target that stopped compiling or an archive
# that lost a file is a tag that published nothing. This builds the whole
# release matrix the way release.yml does, minus the publish and the
Expand Down
32 changes: 32 additions & 0 deletions .scripts/release-launchers.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Build the double-click launchers for a release and attach them to it. Run by the shared
# simtabi/.github go-release.yml as its post-release command, from the repository root, after
# GoReleaser has published the release. GoReleaser does not build these: the macOS app bundle
# around the universal binary it just built, zipped (an .app is a folder), and the Windows GUI
# launchers (src/internal/tools/launchers, as `make app` and `make cross` run it).
#
# Needs TAG, VERSION, DIST (GoReleaser's dist directory), GH_TOKEN, GITHUB_REPOSITORY and
# RUNNER_TEMP, all set in the shared workflow. Writes checksums-launchers.txt beside the assets and
# attaches it too, and copies it to $RUNNER_TEMP/extra-checksums.txt so the launchers are attested.
set -euo pipefail
: "${TAG:?}" "${VERSION:?}" "${DIST:?}" "${GH_TOKEN:?}" "${GITHUB_REPOSITORY:?}" "${RUNNER_TEMP:?}"

out="$RUNNER_TEMP/launchers"
mkdir -p "$out"
# The published icon-sets-sync_macos_universal asset, at the build path GoReleaser recorded for it
# (flatdist reads artifacts.json the same way).
universal=$(jq -r '.[] | select(.name=="icon-sets-sync_macos_universal") | .path' "$DIST/artifacts.json")
[ -f "$universal" ] || { echo "::error::no universal binary in $DIST/artifacts.json"; exit 1; }
go -C src run ./internal/tools/launchers \
-app "$out/icon-sets-sync.app" \
-universal "../$universal" \
-version "$VERSION" \
-windows "$out"
(cd "$out" && zip -qry icon-sets-sync_macos_universal.app.zip icon-sets-sync.app && rm -rf icon-sets-sync.app)
# Written outside the folder first, so it does not list itself.
(cd "$out" && sha256sum -- * > "$RUNNER_TEMP/checksums-launchers.txt")
mv "$RUNNER_TEMP/checksums-launchers.txt" "$out/"
[ "$(find "$out" -name '*.exe' | wc -l)" -ge 3 ] || { echo "::error::expected at least 3 Windows launchers"; exit 1; }
ls -l "$out"
gh release upload "$TAG" --repo "$GITHUB_REPOSITORY" "$out"/*
cp "$out/checksums-launchers.txt" "$RUNNER_TEMP/extra-checksums.txt"
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ The Go module proxy and checksum database already hold `github.com/ichava/icon-s

### Changed

- **A release builds on a pushed version tag, or on request** (owner, 2026-11-13). `release.yml` now holds only those two triggers (`push` of `v[0-9]+.[0-9]+.[0-9]+[-*]`, and `workflow_dispatch -f tag=vX.Y.Z`, replacing the `-f confirm=release-go` gate) and calls `simtabi/.github`'s shared `go-release.yml`, which every Go project uses; the app and launcher step moves to `.scripts/release-launchers.sh`, run as its `post-release`, and now reads the tag from the shared workflow instead of `GITHUB_REF_NAME`, which is a branch on a dispatched run. `TestReleaseRunsOnAVersionTagOrOnRequest` replaces `TestReleaseRunsOnlyOnAConfirmedDispatch` and fails on any other trigger, a branch push, or a pipeline of its own (proven red all three ways).
- **Binaries are published on the GitHub release, and nowhere else** (owner, 2026-11-13). A release, still dispatched by hand with `-f confirm=release-go` for a version the maintainers choose, now also attaches the macOS app (`icon-sets-sync_macos_universal.app.zip`) and the three Windows launchers, built from the released universal binary, with their own `checksums-launchers.txt` and build-provenance attestation. The README and `docs/installation.md` offer the download first and the source build second; there is still no `go install` path, tap, bucket or image, and `TestGoReleaserPublishesNowhereElse` fails if `src/.goreleaser.yaml` gains one (proven red with a `brews` block). The launcher step was run locally against a GoReleaser snapshot: four assets, all verified by their checksums file.
- **CI builds the binaries and the macOS app into `build/` and uploads it.** The Tests workflow's release snapshot is now laid out the way `make cross` and `make app` do it: `build/{os}/` with the 19 binaries, the Linux `.desktop` entry, the Windows launchers and `icon-sets-sync.app`. `check-build` checks it, GoReleaser's scratch directory is gone afterwards, and `build/` is the run's `icon-sets-sync-build` artifact (14 days). Nothing is published; `release.yml` is unchanged. The same sequence run locally gave 37 entries.
- **`.scripts/sync-svg-policy.mjs` checks icon-browser's copy of the SVG policy** (react consolidation 3f). The browser UI's source moved into icon-browser at 3d, so the script reads `icon-browser/resources/js/core/svg-policy.json` instead of the retired `react-icon-browser/src/core/svg-policy.json`. Against core, icon-browser and this repository at `main` it exits 0. `syncscript_test.go` pins the path and runs the script over a temporary workspace, passing on matching copies and failing on a drifted icon-browser copy (both tests proven red against the old path).
Expand Down
8 changes: 4 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ there is how this tool spent weeks re-opening a pull request it had already open

## Releases and versioning

- **Binaries are published on the GitHub release only, and only when the maintainers say so.** A
`vX.Y.Z` tag starts no workflow. `release.yml` is manual and refuses to run without
`-f confirm=release-go`; do not dispatch it, or restore a `push: tags` trigger, without an explicit
maintainer decision for that version. Never add a `go install` path, tap, bucket or image. See
- **Binaries are published on the GitHub release only.** A pushed `vX.Y.Z` tag builds and publishes
the release, and so does `gh workflow run release.yml -f tag=vX.Y.Z`; push a version tag only for
final code. `release.yml` calls `simtabi/.github`'s shared `go-release.yml`: change the pipeline
there, never here. Never add a `go install` path, tap, bucket, image or any other channel. See
[Release](docs/release.md#binaries-are-published-on-the-github-release).
- **A version tag is pushed only for final code.** `vX.Y.Z` marks a release, nothing else. Test a
change on a branch; a pre-release tag (`vX.Y.Z-rc.N`) is for the day binary releases are on.
Expand Down
Loading
Loading