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
117 changes: 95 additions & 22 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ on:
workflow_run:
workflows:
- Tests
branches:
- main
types:
- completed

Expand All @@ -15,12 +17,43 @@ concurrency:
cancel-in-progress: false

jobs:
package:
name: Package ${{ matrix.name }}
version:
name: Determine release version
if: >-
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'main'
github.event.workflow_run.head_branch == 'main' &&
github.event.workflow_run.head_repository.full_name == github.repository
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
version: ${{ steps.version.outputs.version }}
tag: ${{ steps.version.outputs.tag }}
steps:
- name: Check out tested source
uses: actions/checkout@v4
with:
ref: ${{ github.event.workflow_run.head_sha }}
persist-credentials: false

- name: Compute prerelease version
id: version
env:
TEST_RUN_NUMBER: ${{ github.event.workflow_run.run_number }}
run: |
node --input-type=module <<'NODE'
import { readFileSync, appendFileSync } from 'node:fs';
const base = JSON.parse(readFileSync('package.json', 'utf8')).version.replace(/[+-].*$/, '');
if (!/^\d+\.\d+\.\d+$/.test(base) || !/^\d+$/.test(process.env.TEST_RUN_NUMBER)) {
throw new Error('Invalid release version or test run number');
}
const version = `${base}-main.${process.env.TEST_RUN_NUMBER}`;
appendFileSync(process.env.GITHUB_OUTPUT, `version=${version}\ntag=v${version}\n`);
NODE

package:
name: Package ${{ matrix.name }}
needs: version
strategy:
fail-fast: false
matrix:
Expand All @@ -41,13 +74,16 @@ jobs:
artifact: linux
files: release/*.AppImage
runs-on: ${{ matrix.os }}
timeout-minutes: 30
env:
CSC_IDENTITY_AUTO_DISCOVERY: "false"
RELEASE_VERSION: ${{ needs.version.outputs.version }}
steps:
- name: Check out tested source
uses: actions/checkout@v4
with:
ref: ${{ github.event.workflow_run.head_sha }}
persist-credentials: false

- name: Set up Node.js
uses: actions/setup-node@v4
Expand All @@ -58,43 +94,80 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Set installer version
shell: bash
run: npm version "$RELEASE_VERSION" --no-git-tag-version --ignore-scripts

- name: Build package
run: ${{ matrix.command }}
run: ${{ matrix.command }} --publish never

- name: Upload package
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
name: installer-${{ matrix.artifact }}
path: ${{ matrix.files }}
if-no-files-found: error
retention-days: 7
overwrite: true

publish:
name: Publish GitHub release
needs: package
needs: [version, package]
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ needs.version.outputs.tag }}
RELEASE_SHA: ${{ github.event.workflow_run.head_sha }}
steps:
- name: Download packages
uses: actions/download-artifact@v4
with:
path: artifacts
pattern: installer-*
merge-multiple: true

- name: Publish prerelease
uses: softprops/action-gh-release@v2
with:
tag_name: main-${{ github.event.workflow_run.run_number }}
target_commitish: ${{ github.event.workflow_run.head_sha }}
name: Local Forge main build ${{ github.event.workflow_run.run_number }}
prerelease: true
make_latest: false
generate_release_notes: true
body: |
Automated unsigned build from `${{ github.event.workflow_run.head_sha }}`.
- name: Validate installers and generate checksums
working-directory: artifacts
shell: bash
run: |
shopt -s nullglob
for extension in exe dmg AppImage; do
files=(*."$extension")
if [[ ${#files[@]} -ne 1 || ! -s "${files[0]}" ]]; then
echo "::error::Expected one non-empty $extension installer"
exit 1
fi
done
sha256sum -- *.exe *.dmg *.AppImage > SHA256SUMS.txt
sha256sum --check SHA256SUMS.txt

- name: Publish complete prerelease
working-directory: artifacts
shell: bash
run: |
state=$(gh api --paginate "repos/$GH_REPO/releases?per_page=100" \
--jq '.[] | select(.tag_name == env.RELEASE_TAG) | .draft')
if [[ "$state" == "false" ]]; then
echo "Release $RELEASE_TAG is already published; leaving its assets unchanged."
exit 0
fi
if [[ -z "$state" ]]; then
gh release create "$RELEASE_TAG" \
--target "$RELEASE_SHA" \
--title "Local Forge $RELEASE_TAG" \
--draft --prerelease --generate-notes \
--notes "Automated unsigned build from $RELEASE_SHA.

Windows and macOS may show security warnings until release signing is configured.
files: |
artifacts/windows/*.exe
artifacts/macos/*.dmg
artifacts/linux/*.AppImage
Installers: Windows x64 (.exe), macOS universal (.dmg), Linux x64 (.AppImage).
Verify downloads using SHA256SUMS.txt.
Windows and macOS may show security warnings until signing and notarization are configured."
elif [[ "$state" != "true" ]]; then
echo "::error::Unexpected release state: $state"
exit 1
fi
gh release upload "$RELEASE_TAG" --clobber -- *.exe *.dmg *.AppImage SHA256SUMS.txt
gh release edit "$RELEASE_TAG" --draft=false --prerelease --latest=false
5 changes: 3 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ permissions:
contents: read

concurrency:
group: tests-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
group: tests-${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
test:
name: Test, lint, and build
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out source
uses: actions/checkout@v4
Expand Down
13 changes: 12 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ npm run build
3. Update public documentation when contracts or workflows change.
4. Explain user-visible behavior, tradeoffs, and validation in the pull request.

The Tests workflow runs on pull requests and pushes to `main`. A successful push to `main` triggers unsigned native prerelease builds. Do not commit `dist/`, `dist-electron/`, `release/`, local models, imported images, or workspace data.
The Tests workflow runs on pull requests and pushes to `main`. A successful push to `main` triggers unsigned native prerelease builds. New main commits do not cancel checks for earlier commits; obsolete pull-request checks are cancelled. Do not commit `dist/`, `dist-electron/`, `release/`, local models, imported images, or workspace data.

## Release maintenance

- Enable GitHub Actions and allow the actions used by the workflows. The publish job requests `contents: write` for the automatic `GITHUB_TOKEN`; repository or organization policy must permit that permission. No personal access token or publishing secret is required.
- Protect `main` with pull requests and the **Test, lint, and build** required check. Direct pushes also release, so restrict them if releases should only follow merges.
- The Release workflow accepts only successful Tests runs from pushes to this repository's `main`. Pull-request runs, including forks, never publish. Packaging checks out the tested SHA, not the current branch tip.
- The numeric base version comes from `package.json`; any existing prerelease/build suffix is replaced with `-main.<Tests run number>`. To advance the base version, use `npm version <version> --no-git-tag-version` locally and commit both `package.json` and `package-lock.json` in a pull request. CI sets the matching installer version only in its build workspace; it does not push version commits or trigger a release loop.
- Packaging jobs have read-only repository access and explicitly disable electron-builder publishing. Only the final publish job has write access. A release remains a draft until all three non-empty installers and their SHA-256 checksums have been uploaded.
- If packaging or publishing fails, rerun failed jobs from the **Release** workflow in Actions. A full rerun is also safe. Rerunning the same Tests run retains its run number and therefore its release tag. An incomplete draft is reused and its assets replaced; an already published release is left unchanged.
- Installers are retained as workflow artifacts for seven days; published release assets persist. If artifacts have expired, rerun all Release jobs to rebuild them.
- Signing and macOS notarization are not configured. Keep these builds as prereleases until signing credentials and a stable-release policy are established; the pipeline deliberately does not replace the latest stable release.

By contributing, you agree that your contributions are licensed under the [MIT License](LICENSE).
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,23 @@ npm run lint
npm run build
```

`npm run dist` builds an installer for the current platform. Successful test runs on `main` also publish unsigned Windows, macOS, and Linux prereleases through GitHub Actions.
`npm run dist` builds an installer for the current platform.

## Automated releases

Every push to `main` (including a merged pull request) runs tests, lint, and the application build. After those checks pass, GitHub Actions packages that exact commit for Windows x64 (`.exe`), macOS universal (`.dmg`, Intel and Apple Silicon), and Linux x64 (`.AppImage`).

Download installers from [GitHub Releases](https://github.com/Azayzel/local-forge/releases). Each main build is an unsigned prerelease, tagged `v<package-version>-main.<test-run-number>` (for example, `v0.1.0-main.42`). The installer version matches the tag without its `v` prefix. These alpha builds are not marked as the latest stable release.

All three installers and `SHA256SUMS.txt` are uploaded before the release is published. To verify a download, compare its SHA-256 hash with the matching entry in that file:

```powershell
Get-FileHash .\Local-Forge-0.1.0-main.42-win-x64.exe -Algorithm SHA256
```

Use your downloaded installer's actual filename. On Linux, download all three installers and the checksum file into one directory and run `sha256sum --check SHA256SUMS.txt`; on macOS use `shasum -a 256 -c SHA256SUMS.txt`.

Windows and macOS may show security warnings: these builds are not yet signed or notarized. See [release maintenance](CONTRIBUTING.md#release-maintenance) for setup, versioning, and retries.

## Project status

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"build": {
"appId": "io.localforge.desktop",
"productName": "Local Forge",
"artifactName": "Local-Forge-${version}-${os}-${arch}.${ext}",
"icon": "build/icon.png",
"asar": true,
"directories": {
Expand Down
Loading