diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 38832457..4d2cf730 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,6 +3,7 @@ # Push to `release` to build packages and create/update a DRAFT GitHub Release. The version # comes from [workspace.package] in Cargo.toml. An agent verifies the artifacts and publishes # the draft separately; see docs/releasing.md. Tags and pushes to main do not trigger releases. +# At midnight Eastern, changed main commits build and publish a nightly pre-release. # A version with a suffix (0.2.0-rc.1) is a pre-release. Nothing is signed yet. # # Linux x86_64, aarch64 AppImage (+ .zsync), .deb, .rpm, .flatpak, .tar.gz packaging/linux @@ -15,6 +16,9 @@ name: Release on: + schedule: + - cron: '0 0 * * *' + timezone: America/New_York push: branches: [release] workflow_dispatch: @@ -39,17 +43,30 @@ jobs: version: runs-on: ubuntu-latest outputs: + build: ${{ steps.changed.outputs.build }} version: ${{ steps.v.outputs.version }} date: ${{ steps.v.outputs.date }} prerelease: ${{ steps.v.outputs.prerelease }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + - id: changed + name: Skip unchanged nightly builds + env: + GH_TOKEN: ${{ github.token }} + run: bash packaging/nightly.sh - id: v + if: steps.changed.outputs.build == 'true' name: Resolve version env: REQUEST_DRAFT: ${{ inputs.draft || false }} run: | . packaging/env.sh + if [ "$GITHUB_EVENT_NAME" = schedule ]; then + # Unique SemVer without committing daily version bumps to main. + VERSION="${VERSION%%-*}-nightly.$(TZ=America/New_York date +%Y%m%d).$GITHUB_RUN_NUMBER.$GITHUB_RUN_ATTEMPT" + fi if ! printf '%s' "$VERSION" | grep -Eq '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?$'; then echo "::error::Cargo.toml must contain a version like 1.2.3 or 1.2.3-rc.1" exit 1 @@ -65,6 +82,8 @@ jobs: # The app icon in every size, drawn by solvecraft-cli (no image files are committed), for the # Windows jobs: the ARM64 build is cross-compiled and can't run there to draw its own. icons: + needs: version + if: needs.version.outputs.build == 'true' runs-on: ubuntu-22.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -85,6 +104,7 @@ jobs: linux: name: Linux ${{ matrix.arch }} needs: version + if: needs.version.outputs.build == 'true' # The oldest GitHub-hosted image, so the binaries only need glibc >= 2.35. runs-on: ${{ matrix.runner }} strategy: @@ -135,6 +155,7 @@ jobs: flatpak: name: Flatpak ${{ matrix.arch }} needs: [version, linux] + if: needs.version.outputs.build == 'true' # Repackages the Linux job's tarball (the same binaries as the AppImage/deb/rpm); no Rust # build here. 24.04 for a newer flatpak-builder than 22.04 ships. runs-on: ${{ matrix.runner }} @@ -172,6 +193,7 @@ jobs: freebsd: name: FreeBSD x86_64 needs: version + if: needs.version.outputs.build == 'true' # GitHub has no FreeBSD runners: build in a FreeBSD 14.3 VM. runs-on: ubuntu-latest timeout-minutes: 180 @@ -212,6 +234,7 @@ jobs: macos: name: macOS universal needs: version + if: needs.version.outputs.build == 'true' runs-on: macos-14 env: SOLVECRAFT_VERSION: ${{ needs.version.outputs.version }} @@ -235,6 +258,7 @@ jobs: windows: name: Windows ${{ matrix.arch }} needs: [version, icons] + if: needs.version.outputs.build == 'true' runs-on: windows-latest strategy: fail-fast: false @@ -277,6 +301,7 @@ jobs: web: name: Web (wasm) needs: version + if: needs.version.outputs.build == 'true' runs-on: ubuntu-latest env: SOLVECRAFT_VERSION: ${{ needs.version.outputs.version }} @@ -302,8 +327,9 @@ jobs: if-no-files-found: error release: - name: Draft GitHub Release or dry-run artifacts + name: GitHub Release or dry-run artifacts needs: [version, linux, flatpak, freebsd, macos, windows, web] + if: needs.version.outputs.build == 'true' runs-on: ubuntu-latest permissions: contents: write @@ -323,6 +349,18 @@ jobs: run: | (cd artifacts && sha256sum -- * | tee SHA256SUMS.txt) sed "s/{version}/$VERSION/g" .github/release-notes.md >notes.md + - name: Publish nightly pre-release + if: github.event_name == 'schedule' + working-directory: artifacts + run: | + tag="v$VERSION" + sha256sum --check SHA256SUMS.txt + # Stage all packages before publishing. Published tags are never replaced. + gh release create "$tag" --draft --prerelease --latest=false \ + --title "SolveCraft v$VERSION (nightly)" --target "$GITHUB_SHA" \ + --notes-file ../notes.md --generate-notes -- * + gh release edit "$tag" --draft=false --prerelease --latest=false + echo "### Published nightly: $tag (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY" - name: Create or update the draft release if: github.ref == 'refs/heads/release' && (github.event_name == 'push' || inputs.draft) working-directory: artifacts diff --git a/docs/releasing.md b/docs/releasing.md index 44b8fb3f..588162d3 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -6,6 +6,27 @@ GitHub Release. Publishing that verified draft is a separate operation. Pushing pushing a tag does not start the release workflow. There is no automatic commit-message-based version bump and no patch release for every merge. +## Nightly prereleases + +`release.yml` also runs at midnight in `America/New_York` (Eastern time, following daylight +saving time). GitHub may start scheduled runs later when its runners are busy. +Before building, it compares the exact scheduled `main` commit with the tag of the most +recently published release, including prereleases. If the commits match, all packaging and +publication jobs are skipped: no new version, tag, or release is created. Drafts do not count +as published releases, so a failed build or publication can be retried the following night. + +Changed commits use the existing package build and validation jobs. Only after all jobs +succeed, the workflow stages the assets in a draft and publishes a prerelease named +`v-nightly...` with checksums and generated +notes. Any existing workspace prerelease suffix is replaced by the nightly suffix. Dates +use Eastern time; package versions are overridden without changing Cargo.toml or Cargo.lock, +so the binaries' embedded Cargo version remains the workspace version. Nightlies do not +replace the latest stable release or advance the `release` branch. The normal deliberate +release process below continues to create drafts for separate verification and publication. + +Nightly publication uses `GITHUB_TOKEN`, so it does not trigger the downstream Flatpak Pages +deployment. The stable Flatpak repository is not automatically updated with nightly builds. + ## Choose and prepare the version 1. Fetch `origin` and its tags. Inspect the previous published release and the changes since diff --git a/packaging/nightly.sh b/packaging/nightly.sh new file mode 100644 index 00000000..2a16ff2e --- /dev/null +++ b/packaging/nightly.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Decide before building or assigning a nightly version. Checkout must include all tags. +set -euo pipefail + +if [ "$GITHUB_EVENT_NAME" != schedule ]; then + echo 'build=true' >>"$GITHUB_OUTPUT" + exit 0 +fi + +# /releases/latest excludes prereleases. Include every published release instead, +# including nightlies, and sort by publication time across all API pages. +latest_tag="$(gh api --paginate --slurp "repos/$GITHUB_REPOSITORY/releases?per_page=100" \ + --jq '[.[][] | select(.draft == false and .published_at != null)] | sort_by(.published_at) | last | .tag_name // empty')" +if [ -n "$latest_tag" ]; then + # Resolve the tag itself, not target_commitish (which can be a moving branch). + released_sha="$(git rev-parse --verify "refs/tags/$latest_tag^{commit}")" + if [ "$released_sha" = "$GITHUB_SHA" ]; then + echo 'build=false' >>"$GITHUB_OUTPUT" + echo "### Nightly skipped: no new commits since $latest_tag" >>"$GITHUB_STEP_SUMMARY" + exit 0 + fi +fi + +echo 'build=true' >>"$GITHUB_OUTPUT"