diff --git a/.github/release-notes.md b/.github/release-notes.md new file mode 100644 index 0000000..5ac0461 --- /dev/null +++ b/.github/release-notes.md @@ -0,0 +1,27 @@ +FieldCraft is open-source CAE pre-processing in Rust: it turns geometry into analysis-ready +finite element models, writes Abaqus, Nastran and LS-DYNA decks, and solves linear static and +modal problems with its own solver. AI agents can drive it over MCP (`fieldcraft-cli mcp`). +[README](https://github.com/jyviko/fieldcraft#readme) · +[roadmap](https://github.com/jyviko/fieldcraft/blob/main/ROADMAP.md) · +[issues](https://github.com/jyviko/fieldcraft/issues) + +**Which file?** +- **macOS 11+** (Apple silicon and Intel): open `fieldcraft-{version}-macos-universal.dmg` and + drag `FieldCraft.app` onto Applications. The command-line tool is in + `fieldcraft-cli-{version}-macos-universal.zip`. +- **Linux** (x86_64, glibc 2.35+: Ubuntu 22.04, Debian 12, Fedora 36 or newer): + `fieldcraft-{version}-linux-x86_64.AppImage` (`chmod +x`, then run it), or unpack + `fieldcraft-{version}-linux-x86_64.tar.gz` and run `bin/fieldcraft` (`bin/fieldcraft-cli` is the + command-line tool). +- **Windows 10+** (x64): unzip `fieldcraft-{version}-windows-x64.zip` and run `fieldcraft.exe` + (`fieldcraft-cli.exe` is the command-line tool). +- `SHA256SUMS.txt`: `sha256sum -c SHA256SUMS.txt --ignore-missing` (macOS: + `shasum -a 256 -c SHA256SUMS.txt --ignore-missing`). + +To start with a sample model, run the app with `--sample cantilever` (macOS: +`/Applications/FieldCraft.app/Contents/MacOS/FieldCraft --sample cantilever`). + +**Signing.** {macos_signing} The Linux and Windows builds are not signed; on Windows, SmartScreen +may say "Windows protected your PC": choose **More info**, then **Run anyway**. + +Every package carries `LICENSE-MIT`, `LICENSE-APACHE`, `NOTICE` and `THIRD-PARTY-LICENSES.txt`. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0445936 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,252 @@ +# Release builds (docs/releasing.md), after SolidCraft's and VectorCraft's release.yml +# (MIT OR Apache-2.0; see ATTRIBUTION.md). +# +# A push to `release` (fast-forwarded from main) builds every package and creates or updates the +# DRAFT GitHub Release "FieldCraft v", with SHA256SUMS.txt and generated notes. The +# version is [workspace.package] version in Cargo.toml; a suffix (0.2.0-rc.1) makes a +# pre-release. A person verifies the draft and publishes it; this workflow never publishes, +# and pushes to main or tags never start it. +# +# macOS universal (arm64 + x86_64) .dmg with FieldCraft.app, the CLI as a .zip packaging/macos +# Linux x86_64 (glibc 2.35+) .tar.gz and .AppImage packaging/linux +# Windows x64 .zip with fieldcraft.exe and fieldcraft-cli.exe packaging/windows +# +# macOS: signed with a Developer ID and notarized when the APPLE_* secrets exist, ad-hoc +# otherwise (the release notes say which). Linux and Windows builds are not signed. +# +# Dry run: `gh workflow run release.yml --ref ` builds everything and uploads the +# `release-assets` artifact without creating a release. `-f draft=true` on `release` retries +# the draft. +name: Release + +on: + push: + branches: [release] + workflow_dispatch: + inputs: + draft: + description: "Create/update the draft release (release branch only; off: build artifacts only)" + type: boolean + default: false + +permissions: + contents: read + +concurrency: + group: release-${{ github.ref }} + # Never cancel a run mid-notarization or mid-upload; a newer push waits its turn. + cancel-in-progress: false + +env: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + +jobs: + version: + runs-on: ubuntu-24.04 + outputs: + version: ${{ steps.v.outputs.version }} + prerelease: ${{ steps.v.outputs.prerelease }} + draft: ${{ steps.v.outputs.draft }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - id: v + name: Resolve the version + env: + REQUEST_DRAFT: ${{ github.event_name == 'push' || inputs.draft }} + run: | + . packaging/env.sh + if ! printf '%s' "$VERSION" | grep -Eq '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-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, not '$VERSION'" + exit 1 + fi + if [ "$REQUEST_DRAFT" = true ] && [ "$GITHUB_REF" != refs/heads/release ]; then + echo "::error::Draft releases are built from the release branch only" + exit 1 + fi + case "$VERSION" in *-*) pre=true ;; *) pre=false ;; esac + { echo "version=$VERSION"; echo "prerelease=$pre"; echo "draft=$REQUEST_DRAFT"; } >>"$GITHUB_OUTPUT" + echo "### FieldCraft v$VERSION (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY" + + macos: + name: macOS universal + needs: version + runs-on: macos-14 + timeout-minutes: 120 + outputs: + signing: ${{ steps.package.outputs.signing }} + env: + FIELDCRAFT_VERSION: ${{ needs.version.outputs.version }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-apple-darwin, x86_64-apple-darwin + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + key: release-macos-universal + # Release runs are rare: only release-branch runs keep a cache, so dry runs don't evict CI's. + save-if: ${{ github.ref == 'refs/heads/release' }} + - name: Import the signing certificate (skipped without APPLE_CERTIFICATE) + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + run: packaging/macos/import-cert.sh + - name: Build, sign, notarize and package (the dmg is mounted and checked) + id: package + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: packaging/macos/package.sh --arch universal + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: release-macos + path: dist/release/* + if-no-files-found: error + - name: Remove the temporary keychain + if: always() + run: | + if [ -n "${MACOS_KEYCHAIN:-}" ]; then security delete-keychain "$MACOS_KEYCHAIN" || true; fi + + linux: + name: Linux x86_64 + needs: version + # The oldest GitHub-hosted image, so the binaries need only glibc >= 2.35. + runs-on: ubuntu-22.04 + timeout-minutes: 90 + env: + FIELDCRAFT_VERSION: ${{ needs.version.outputs.version }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install packaging tools + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends desktop-file-utils + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + key: release-linux-x86_64 + # Release runs are rare: only release-branch runs keep a cache, so dry runs don't evict CI's. + save-if: ${{ github.ref == 'refs/heads/release' }} + - name: Build and package (tar.gz, AppImage) + run: packaging/linux/package.sh + - name: Check the packages + run: | + . packaging/env.sh + list="$(tar -tzf dist/release/*.tar.gz)" + for f in bin/fieldcraft bin/fieldcraft-cli "share/applications/$APP_ID.desktop" \ + share/doc/fieldcraft/THIRD-PARTY-LICENSES.txt; do + grep -Fx -- "fieldcraft-$VERSION-linux-x86_64/$f" <<<"$list" + done + ldd target/release/fieldcraft + for appimage in dist/release/*.AppImage; do + chmod +x "$appimage" + APPIMAGE_EXTRACT_AND_RUN=1 "$appimage" --version + done + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: release-linux-x86_64 + path: dist/release/* + if-no-files-found: error + + windows: + name: Windows x64 + needs: version + runs-on: windows-latest + timeout-minutes: 90 + env: + FIELDCRAFT_VERSION: ${{ needs.version.outputs.version }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-pc-windows-msvc + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + key: release-windows-x64 + # Release runs are rare: only release-branch runs keep a cache, so dry runs don't evict CI's. + save-if: ${{ github.ref == 'refs/heads/release' }} + - name: Build and package (zip) + shell: pwsh + run: ./packaging/windows/package.ps1 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: release-windows-x64 + path: dist/release/* + if-no-files-found: error + + release: + name: Draft release (or dry-run artifacts) + needs: [version, macos, linux, windows] + runs-on: ubuntu-24.04 + permissions: + contents: write + env: + VERSION: ${{ needs.version.outputs.version }} + PRERELEASE: ${{ needs.version.outputs.prerelease }} + MACOS_SIGNING: ${{ needs.macos.outputs.signing }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + pattern: release-* + path: artifacts + merge-multiple: true + - name: Check the set, checksums and notes + run: | + for f in "fieldcraft-$VERSION-macos-universal.dmg" "fieldcraft-cli-$VERSION-macos-universal.zip" \ + "fieldcraft-$VERSION-linux-x86_64.tar.gz" "fieldcraft-$VERSION-linux-x86_64.AppImage" \ + "fieldcraft-$VERSION-windows-x64.zip"; do + test -s "artifacts/$f" || { echo "::error::missing $f"; exit 1; } + done + (cd artifacts && sha256sum -- * | tee SHA256SUMS.txt) + # The macOS paragraph says how the app was signed (packaging/macos/package.sh). + case "$MACOS_SIGNING" in + notarized) macos="The macOS app is signed with a Developer ID and notarized by Apple." ;; + signed) macos="The macOS app is signed with a Developer ID but **not notarized**: the first time, right-click \`FieldCraft.app\` in Applications and choose **Open**." ;; + *) macos="The macOS app is **ad-hoc signed and not notarized**: macOS blocks the first launch. Right-click \`FieldCraft.app\` in Applications and choose **Open** (then **Open** again), or run \`xattr -dr com.apple.quarantine /Applications/FieldCraft.app\`." ;; + esac + awk -v v="$VERSION" -v m="$macos" '{ gsub(/\{version\}/, v); gsub(/\{macos_signing\}/, m); print }' \ + .github/release-notes.md >notes.md + cat notes.md >>"$GITHUB_STEP_SUMMARY" + - name: Upload the dry-run artifacts + if: needs.version.outputs.draft != 'true' + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: release-assets + path: | + artifacts/* + notes.md + if-no-files-found: error + - name: Create or update the draft release + if: needs.version.outputs.draft == 'true' + working-directory: artifacts + run: | + tag="v$VERSION" + title="FieldCraft v$VERSION" + if gh api "repos/$GH_REPO/git/matching-refs/tags/$tag" --jq '.[].ref' | grep -Fxq "refs/tags/$tag"; then + echo "::error::Tag $tag already exists. Choose a new version; never move a release tag." + exit 1 + fi + if draft="$(gh release view "$tag" --json isDraft --jq .isDraft 2>/dev/null)"; then + if [ "$draft" != true ]; then + echo "::error::Release $tag is already published. Bump the version before releasing." + exit 1 + fi + # A rebuild of the same version replaces the draft's assets (removing names it no longer + # makes); its notes are kept. + assets="$(gh release view "$tag" --json assets --jq '.assets[].name')" + while IFS= read -r asset; do + if [ -n "$asset" ]; then gh release delete-asset "$tag" "$asset" --yes; fi + done <<<"$assets" + gh release edit "$tag" --draft --title "$title" --target "$GITHUB_SHA" --prerelease="$PRERELEASE" + gh release upload "$tag" -- * + else + gh release create "$tag" --draft --title "$title" --target "$GITHUB_SHA" \ + --notes-file ../notes.md --generate-notes --prerelease="$PRERELEASE" -- * + fi + echo "Draft release **$title** at \`${GITHUB_SHA::9}\`: $(gh release view "$tag" --json url --jq .url)" >>"$GITHUB_STEP_SUMMARY" diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 8cc7865..3c51e8e 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -175,6 +175,7 @@ is committed and listed here with its exports. | packaging/env.sh, packaging/macos/package.sh, packaging/macos/Info.plist.in, packaging/linux/package.sh, packaging/linux/AppRun, packaging/windows/package.ps1 | SolidCraft `packaging/` and VectorCraft `packaging/` (signing and notarization in macos/package.sh) | MIT OR Apache-2.0 | | packaging/macos/import-cert.sh | VectorCraft `packaging/macos/import-cert.sh` | MIT OR Apache-2.0 | | packaging/windows/check-exe.ps1 | SolidCraft `packaging/windows/check-icon.ps1` | MIT OR Apache-2.0 | +| .github/workflows/release.yml, .github/release-notes.md, docs/releasing.md | SolidCraft `.github/workflows/release.yml`, `.github/release-notes.md`, `docs/releasing.md`; VectorCraft `.github/workflows/release.yml` | MIT OR Apache-2.0 | ## Test oracles diff --git a/README.md b/README.md index 1fe6440..071e429 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ Rust 1.95 or later, with the `wasm32-unknown-unknown` target for `cargo xtask ci - [Libraries and licences](docs/adr/0001-libraries.md) - [AI agents (MCP)](docs/mcp.md): `claude mcp add fieldcraft -- /path/to/fieldcraft-cli mcp` - [Control protocol](docs/control-protocol.md): driving the desktop app +- [Releasing](docs/releasing.md): how a release is built, verified and published - [Contributing](CONTRIBUTING.md) and [agent instructions](AGENTS.md) ## License diff --git a/assets/app-icon/README.md b/assets/app-icon/README.md index 015fe22..857a6f7 100644 --- a/assets/app-icon/README.md +++ b/assets/app-icon/README.md @@ -22,6 +22,8 @@ hand. It runs on macOS (sips, iconutil, `xcrun actool`) with ImageMagick (`magic | `Assets.car` | `actool` compilation of `FieldCraft.icon` for macOS 26 and later (needs Xcode 26 or later; the task skips it with a note otherwise) | The desktop app embeds `icon.iconset/icon_128x128@2x.png` as its window and Dock icon -(`apps/fieldcraft/src/main.rs`). There is no macOS bundle yet; packaging will copy -`Assets.car` and `icon.icns` into `Contents/Resources` and name `FieldCraft` in -`CFBundleIconName` / `CFBundleIconFile`. +(`apps/fieldcraft/src/main.rs`). The release packages (docs/releasing.md) use the rest: +`packaging/macos/package.sh` copies `Assets.car` and `icon.icns` (as `FieldCraft.icns`) into +`FieldCraft.app/Contents/Resources`, named by `CFBundleIconName` / `CFBundleIconFile`; +`apps/fieldcraft/build.rs` embeds `icon.ico` in `fieldcraft.exe`; and +`packaging/linux/package.sh` installs the iconset PNGs as the hicolor icons. diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 0000000..81dfae7 --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,103 @@ +# Releasing FieldCraft + +Releases are deliberate. Development lands on `main`; pushing the chosen commit to the `release` +branch builds every package and creates a **draft** GitHub Release; a maintainer verifies the +draft and publishes it. Pushes to `main` and tags never start a release, and nothing bumps the +version automatically. The workflow is `.github/workflows/release.yml`; the packaging scripts +are in `packaging/` and run the same way on a developer machine. + +## What a release contains + +| Platform | Files | Script | +|---|---|---| +| macOS 11+, universal (arm64 + x86_64) | `fieldcraft--macos-universal.dmg` (FieldCraft.app on a drag-to-Applications image), `fieldcraft-cli--macos-universal.zip` | `packaging/macos/package.sh` | +| Linux x86_64, glibc 2.35+ | `fieldcraft--linux-x86_64.tar.gz` (`bin/`, `share/`), `fieldcraft--linux-x86_64.AppImage` | `packaging/linux/package.sh` | +| Windows x64 | `fieldcraft--windows-x64.zip` (`fieldcraft.exe`, `fieldcraft-cli.exe`) | `packaging/windows/package.ps1` | + +Plus `SHA256SUMS.txt` and the notes from `.github/release-notes.md` followed by GitHub's +generated list of changes. Every package carries `README.md`, `LICENSE-MIT`, `LICENSE-APACHE`, +`NOTICE` and `THIRD-PARTY-LICENSES.txt` (on macOS in `FieldCraft.app/Contents/Resources/Licenses`). +The application id is `io.github.jyviko.fieldcraft`: the macOS bundle id, the Linux `.desktop` +entry and icon name, and the window's Wayland app_id / X11 class. The icons come from +`assets/app-icon`: `icon.icns` and `Assets.car` (macOS 26 and later) in the app bundle, +`icon.ico` embedded in `fieldcraft.exe` (`apps/fieldcraft/build.rs`), the iconset PNGs as the +Linux hicolor icons. + +## Signing + +**macOS.** When the repository has the secrets below, the app, the dmg and the CLI are signed +with a Developer ID (hardened runtime, `packaging/macos/entitlements.plist`) and notarized, and +the tickets are stapled. Without them the build is **ad-hoc signed**, which runs on Apple silicon +but which Gatekeeper blocks on first launch until the user right-clicks › Open; a run warns +about it, and the release notes say which of the two the draft contains. + +| Secret | What it is | +|---|---| +| `APPLE_CERTIFICATE` | The "Developer ID Application" certificate with its private key, exported as .p12 and base64-encoded (`base64 -i cert.p12 \| pbcopy`) | +| `APPLE_CERTIFICATE_PASSWORD` | The .p12's password | +| `KEYCHAIN_PASSWORD` | Optional: the temporary CI keychain's password (random when unset) | +| `APPLE_ID` | The Apple ID used for notarization | +| `APPLE_PASSWORD` | An app-specific password for that Apple ID (appleid.apple.com › Sign-In and Security) | +| `APPLE_TEAM_ID` | The 10-character team id of the Developer ID | + +Add them with `gh secret set -R jyviko/fieldcraft`. With the certificate but without the +three notarization secrets the app is signed and not notarized. A dmg made locally with +`packaging/macos/package.sh` is ad-hoc signed unless `MACOS_SIGN_IDENTITY` names an identity +in the keychain. + +**Linux and Windows** builds are not signed; the checksums are the integrity check. On Windows +SmartScreen warns about an unsigned download (More info › Run anyway) until a code-signing +certificate is added. + +## Choose and prepare the version + +1. Look at the previous published release (`gh release list -R jyviko/fieldcraft`) and the + changes since it, and choose one version for the batch: a patch for compatible fixes; a minor + for new capabilities, and while on `0.x` also for breaking changes (say what breaks in the + notes); `-rc.N` for a release candidate. `1.0` is the owner's decision. +2. Set `[workspace.package] version` in the root `Cargo.toml` and run + `cargo update --workspace --offline`, which changes only the workspace crates' versions in + `Cargo.lock`. Land it on `main` through a pull request like any change, and let CI on `main` + pass. Note the commit to release. + +## Build the draft + +```sh +git fetch origin +git switch release # the first time: git switch -c release origin/main +git merge --ff-only origin/main +git push origin release +``` + +Only fast-forward `release` and never force-push it. The run builds the three platforms; when +all succeed, the last job checks that every file is there, writes `SHA256SUMS.txt` and the notes, +and creates the draft `FieldCraft v` targeting the pushed commit. Pushing +`release` again with the same version rebuilds and replaces the draft's files (its notes are +kept). The job refuses a version whose tag exists or whose release is already published: bump +the version instead. + +## Verify and publish + +Publishing is a maintainer's call; an agent preparing a release stops at the draft. + +1. Check the run succeeded for the intended commit and the draft targets it + (`gh release view v -R jyviko/fieldcraft`). +2. Download the assets into an empty folder (`gh release download v -R jyviko/fieldcraft`), + run `shasum -a 256 -c SHA256SUMS.txt`, and check every platform's files are there. +3. Smoke-test what you can run: open the dmg, drag the app to Applications, launch it with + `--sample cantilever`; run `fieldcraft-cli --version` from the zip or tar.gz; on Linux run the + AppImage; on Windows unzip and start `fieldcraft.exe`. Look at the run's logs for the + platforms you could not try (each job checks its own package). +4. Edit the notes: what is new, fixes, breaking changes, known limitations. Keep the install and + signing paragraphs; never claim signing the notes don't state. +5. Publish: `gh release edit v -R jyviko/fieldcraft --draft=false --latest` + (a release candidate: `--draft=false --prerelease --latest=false`). Publishing creates the tag + at the draft's commit; never push release tags by hand. + +## Dry runs + +`gh workflow run release.yml -R jyviko/fieldcraft --ref ` builds every package and +uploads them with the notes as the `release-assets` artifact of the run, without creating a +release. `-f draft=true` creates or updates the draft, and is accepted on `release` only. +Locally, `packaging/macos/package.sh --arch aarch64` (or `x86_64`, `universal`) and +`packaging/linux/package.sh` write to `dist/release/`.