Skip to content

release

release #32

Workflow file for this run

name: release
# Manually-triggered stable release. Pick a bump (patch/minor/major) —
# the next vX.Y.Z tag is computed from the latest stable tag. The `notes`
# input is used verbatim as the release body and is also attached to the
# release as `CHANGELOG.md`. An ephemeral release commit (with the built
# ui/dist/ force-added past .gitignore) is created on a detached HEAD and
# the new tag points to *that* commit, so `go install ...@vX.Y.Z` resolves
# a tree containing the embedded UI assets. Main itself is never modified
# — only the tag is pushed to origin.
on:
workflow_dispatch:
inputs:
bump:
description: "Semver bump (patch/minor/major)"
required: true
type: choice
default: patch
options:
- patch
- minor
- major
notes:
description: "Release notes in Markdown. Becomes the release body and the attached CHANGELOG.md asset. Use \\n for newlines if passing via CLI."
required: true
type: string
permissions: read-all
concurrency:
group: release-manual
cancel-in-progress: false
jobs:
tag:
name: compute next tag
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
tag: ${{ steps.ver.outputs.tag }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Compute next stable tag
id: ver
env:
BUMP: ${{ inputs.bump }}
run: |
set -eu
latest=$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
| sort -V \
| tail -1)
if [ -z "$latest" ]; then
latest="v0.0.0"
fi
ver="${latest#v}"
major="${ver%%.*}"
rest="${ver#*.}"
minor="${rest%%.*}"
patch="${rest##*.}"
case "$BUMP" in
major) major=$((major+1)); minor=0; patch=0 ;;
minor) minor=$((minor+1)); patch=0 ;;
patch) patch=$((patch+1)) ;;
*) echo "unknown bump: $BUMP" >&2; exit 1 ;;
esac
next="v${major}.${minor}.${patch}"
echo "tag=$next" >> "$GITHUB_OUTPUT"
echo "Next tag: $next (from $latest, bump=$BUMP)"
ui:
name: build ui
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: ui/package-lock.json
- run: npm --prefix ui ci
- run: npm --prefix ui run build
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ui-dist
path: ui/dist
retention-days: 1
if-no-files-found: error
build:
name: build ${{ matrix.goos }}-${{ matrix.goarch }}
needs: [tag, ui]
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-latest
goos: linux
goarch: amd64
- runner: macos-latest
goos: darwin
goarch: arm64
runs-on: ${{ matrix.runner }}
permissions:
contents: read
env:
CGO_ENABLED: "1"
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version-file: go.mod
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ui-dist
path: ui/dist
- name: Build binary
run: |
set -eu
tag="${{ needs.tag.outputs.tag }}"
sha="${{ github.sha }}"
date="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
pkg="github.com/RandomCodeSpace/docsiq/cmd"
ldflags="-s -w -X ${pkg}.Version=${tag} -X ${pkg}.Commit=${sha} -X ${pkg}.Date=${date}"
go build -tags sqlite_fts5 -trimpath -ldflags="${ldflags}" -o docsiq ./
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: binary-${{ matrix.goos }}-${{ matrix.goarch }}
path: docsiq
retention-days: 1
if-no-files-found: error
release:
name: publish release
needs: [tag, build]
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write # cosign keyless
attestations: write # SLSA provenance
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
with:
# v2.x — v3 broke our sign-blob flag compatibility.
cosign-release: 'v2.6.3'
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: binary-*
path: downloaded/
- name: Prepare release notes
id: notes
env:
TAG: ${{ needs.tag.outputs.tag }}
NOTES: ${{ inputs.notes }}
run: |
set -eu
if ! printf '%s' "$NOTES" | grep -Eq '[^[:space:]]'; then
echo "::error::The 'notes' workflow input is empty. Fire release.yml again with curated Markdown notes."
exit 1
fi
mkdir -p dist
# CHANGELOG.md attached to the release as-is (same bytes as the body).
printf '# %s\n\n%s\n' "$TAG" "$NOTES" > dist/CHANGELOG.md
# Emit the body to GITHUB_OUTPUT for the next step. The Verify
# footer is appended in the create-release step.
{
echo 'body<<__RN_EOF__'
printf '%s' "$NOTES"
echo
echo '__RN_EOF__'
} >> "$GITHUB_OUTPUT"
- name: Assemble versioned binaries + SHA256SUMS
env:
TAG: ${{ needs.tag.outputs.tag }}
run: |
set -eu
mkdir -p dist
for dir in downloaded/binary-*; do
rest=$(basename "$dir" | sed 's/^binary-//')
goos="${rest%-*}"
goarch="${rest##*-}"
out="dist/docsiq-${TAG}-${goos}-${goarch}"
cp "$dir/docsiq" "$out"
chmod +x "$out"
done
(cd dist && sha256sum docsiq-* > SHA256SUMS)
ls -la dist/
- name: Sign artifacts with cosign (keyless)
run: |
set -eu
cd dist
for f in docsiq-* SHA256SUMS; do
cosign sign-blob --yes \
--output-signature="${f}.sig" \
--output-certificate="${f}.pem" \
"$f"
done
ls -la
- name: Download ui-dist for release commit
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ui-dist
path: ui/dist
- name: Bake ui/dist into release commit and tag it
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.tag.outputs.tag }}
run: |
set -eu
# ui/dist/ is gitignored on main, so a `go install ...@TAG`
# against the main tree would embed only a placeholder
# index.html and 404 on every /assets/* request. Create an
# ephemeral commit on a detached HEAD that force-includes the
# built ui/dist/, tag that commit as $TAG, and push the tag
# only — main itself is never modified.
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git checkout --detach
git add -f ui/dist
git commit -m "release: bake ui/dist for ${TAG}"
git tag "$TAG"
git push origin "$TAG"
- name: Create GitHub release and upload assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.tag.outputs.tag }}
NOTES_BODY: ${{ steps.notes.outputs.body }}
run: |
set -eu
{
printf '%s\n\n' "$NOTES_BODY"
printf '### Verify\n\n'
printf 'All artifacts are signed with [cosign](https://github.com/sigstore/cosign) keyless via Sigstore.\n\n'
printf '```sh\n'
printf 'cosign verify-blob \\\n'
printf " --certificate-identity-regexp 'https://github.com/RandomCodeSpace/docsiq/\\\\.github/workflows/release\\\\.yml.*' \\\\\n"
printf " --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \\\\\n"
printf ' --certificate docsiq-%s-linux-amd64.pem \\\n' "$TAG"
printf ' --signature docsiq-%s-linux-amd64.sig \\\n' "$TAG"
printf ' docsiq-%s-linux-amd64\n' "$TAG"
printf '```\n'
} > release-notes.md
gh release create "$TAG" \
--title "$TAG" \
--notes-file release-notes.md \
dist/docsiq-* dist/SHA256SUMS dist/SHA256SUMS.sig dist/SHA256SUMS.pem dist/CHANGELOG.md
- name: Generate SLSA build provenance
id: attest
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
subject-path: |
dist/docsiq-*-linux-amd64
dist/docsiq-*-darwin-arm64
dist/SHA256SUMS
- name: Upload provenance to release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.tag.outputs.tag }}
run: |
set -eu
cp "${{ steps.attest.outputs.bundle-path }}" "docsiq-${TAG}.intoto.jsonl"
gh release upload "$TAG" "docsiq-${TAG}.intoto.jsonl"