Rework Gitea CI and cross-platform release builds
CI / pull_request_policy (push) Skipped
CI / markdown (push) Successful in 53s
CI / build (push) Failing after 2m17s

This commit is contained in:
2026-09-24 22:56:40 +02:00
parent 88c2fd34d4
commit fa0864394f
29 changed files with 1202 additions and 1466 deletions
+75
View File
@@ -0,0 +1,75 @@
# Gitea Actions and release setup
Gitea runs CI, pull request checks, translation reports, and the manual release workflow from
`.gitea/workflows`. Gitea reads that directory before `.github/workflows`. The one workflow in
`.github/workflows` runs hosted Windows and macOS packaging jobs on the public
[GitHub mirror](https://github.com/NilsBriggen/Logisim-Revolution). The inherited upstream
GitHub workflows are removed. Gitea actions are fetched from `gitea.com/actions`.
## Gitea runner
The Ubuntu server's existing `act_runner` keeps its `ubuntu-latest` container label for CI.
The manual release additionally needs these two labels:
| Label | Execution image | Purpose |
| --- | --- | --- |
| `linux-amd64` | `catthehacker/ubuntu:act-latest` | JDK 21 build, tests, JAR, sources, DEB and RPM |
| `linux-arm64` | `arm64v8/ubuntu:24.04` | ARM64 DEB and RPM under host QEMU binfmt |
The runner configuration uses `docker://` labels and `container.docker_host: "-"`. Jobs have no
access to the host Docker socket. On the current Ubuntu 26.04 host, install `qemu-user-binfmt`
and check ARM64 execution before starting a release:
```bash
sudo apt-get install qemu-user-binfmt
docker run --rm --platform linux/arm64 ubuntu:24.04 uname -m
```
The check must print `aarch64`. The ARM64 job installs an ARM64 JDK 21 inside its container, so
`jpackage` embeds an ARM64 runtime. It uses the public Gitea source repository at the exact
workflow commit. The x86_64 container installs `fakeroot` and RPM tools during its job. ARM64
emulation is slower than a native ARM machine; the job timeout is four hours.
## GitHub hosted Windows and Mac jobs
The public mirror's `.github/workflows/platforms.yml` uses standard `windows-2022`,
`macos-15-intel`, and `macos-15` runners. Windows Server 2022 includes WiX Toolset 3; the
workflow adds it to `PATH` for `jpackage`. Each job checks that the mirrored commit matches the
Gitea manual run, builds its native package, and uploads a one-day GitHub Actions artifact.
The Gitea workflow downloads these artifacts into a draft Gitea release. The macOS DMGs are
development packages without Apple notarization.
[GitHub's billing documentation](https://docs.github.com/en/billing/concepts/product-billing/github-actions)
says standard hosted runner time is free for public repositories. Do not switch the mirror to
private or a larger runner without checking the billing settings.
## Tokens and manual release
In Gitea **Repository Settings → Actions → General**, enable Actions and permit the job token
**Code: read** and **Releases: write**. The workflow uses its built-in `GITEA_TOKEN` for draft
creation and attachment uploads. Store a GitHub token in the Gitea repository Actions secret
`GH_TOKEN`. It needs access only to the public `NilsBriggen/Logisim-Revolution` mirror with
**Contents: read and write** and **Actions: read and write**. It pushes the exact Gitea commit
to GitHub `main`, dispatches the hosted workflow, checks its result, and downloads its artifacts.
The GitHub workflow receives no Gitea credential.
In Gitea's **Actions** tab, select **Build all platforms and publish release** and run it on
`main`. It creates a draft prerelease tagged `build-<run-id>-attempt-<attempt>`, builds Linux
x86_64 and emulated ARM64 locally, and dispatches the Windows and Mac jobs. It publishes the
Gitea release only when every job succeeds and all ten expected files are present and nonempty.
A failure leaves the draft unpublished for inspection. A rerun uses a new attempt tag.
The ten files are a portable application JAR and source JAR, DEB and RPM for each Linux
architecture, a Windows x86_64 MSI and portable ZIP, and Intel and Apple Silicon DMGs.
Package names are derived from `gradle.properties` and checked before publication. There is
no automatic nightly release or Snap Store upload.
## Other CI checks
`CI` runs the full Gradle build, Java Checkstyle, changed maintained Markdown lint, and PR
changelog and issue checks. A PR may use `NO_CHANGELOG_ENTRY`,
`NO_CHANGELOG_AUTHOR_CREDIT`, or `NO_TICKET` in its description when the corresponding
requirement does not apply. The post-merge workflow locks merged PRs and closed linked issues
using the Gitea API. Translation checks remain advisory because the inherited bundles contain
known issues. Gitea's built-in issue dependencies replace the old GitHub-only action. Gitea
does not support the `security-events` token scope required by the old CodeQL upload.
+63
View File
@@ -0,0 +1,63 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
types: [opened, synchronize, reopened, edited]
permissions:
contents: read
issues: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: https://gitea.com/actions/checkout@v4
- uses: https://gitea.com/actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- name: Check workflow policy and release completeness rules
run: python3 -m unittest discover -s scripts/ci -p 'test_*.py'
- name: Build, test and check Java style
run: ./gradlew build --no-daemon --console=plain
pull_request_policy:
if: gitea.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: https://gitea.com/actions/checkout@v4
with:
fetch-depth: 0
- name: Fetch main for pull request comparisons
run: git fetch origin main
- name: Check changelog and linked issue
env:
GITEA_EVENT_PATH: ${{ gitea.event_path }}
GITEA_SERVER_URL: ${{ gitea.server_url }}
GITEA_API_URL: ${{ gitea.api_url }}
GITEA_REPOSITORY: ${{ gitea.repository }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
NO_CHANGELOG_AUTHOR_CREDIT: ${{ vars.NO_CHANGELOG_AUTHOR_CREDIT }}
run: python3 scripts/ci/pr_checks.py
markdown:
runs-on: ubuntu-latest
steps:
- uses: https://gitea.com/actions/checkout@v4
with:
fetch-depth: 0
- uses: https://gitea.com/actions/setup-node@v4
with:
node-version: '22'
- name: Fetch main for pull request comparisons
if: gitea.event_name == 'pull_request'
run: git fetch origin main
- name: Lint changed Markdown files
env:
GITEA_EVENT_NAME: ${{ gitea.event_name }}
GITEA_EVENT_PATH: ${{ gitea.event_path }}
run: python3 scripts/ci/lint_changed_markdown.py
+27
View File
@@ -0,0 +1,27 @@
name: Lock merged discussions
on:
pull_request_target:
types: [closed]
permissions:
code: read
issues: write
pull-requests: write
jobs:
lock:
runs-on: ubuntu-latest
steps:
# Always execute the script from the trusted main branch.
- uses: https://gitea.com/actions/checkout@v4
with:
ref: main
- name: Lock merged PR and closed linked issues
env:
GITEA_EVENT_PATH: ${{ gitea.event_path }}
GITEA_SERVER_URL: ${{ gitea.server_url }}
GITEA_API_URL: ${{ gitea.api_url }}
GITEA_REPOSITORY: ${{ gitea.repository }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: python3 scripts/ci/post_merge.py
+148
View File
@@ -0,0 +1,148 @@
name: Build all platforms and publish release
on:
workflow_dispatch:
permissions:
code: read
releases: write
jobs:
prepare:
if: gitea.ref == 'refs/heads/main'
runs-on: ubuntu-latest
outputs:
release_id: ${{ steps.draft.outputs.release_id }}
steps:
- uses: https://gitea.com/actions/checkout@v4
with:
fetch-depth: 0
- name: Create a draft release for this run
id: draft
env:
GITEA_API_URL: ${{ gitea.api_url }}
GITEA_REPOSITORY: ${{ gitea.repository }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_SHA: ${{ gitea.sha }}
GITEA_RUN_ID: ${{ gitea.run_id }}
GITEA_RUN_ATTEMPT: ${{ gitea.run_attempt }}
run: python3 scripts/ci/release.py prepare
- name: Mirror this commit and dispatch GitHub Windows and Mac builds
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GITEA_SHA: ${{ gitea.sha }}
RELEASE_TAG: build-${{ gitea.run_id }}-attempt-${{ gitea.run_attempt }}
run: python3 scripts/ci/github_release.py dispatch
linux_amd64:
needs: prepare
runs-on: linux-amd64
timeout-minutes: 120
steps:
- uses: https://gitea.com/actions/checkout@v4
- uses: https://gitea.com/actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- name: Install Linux packaging tools
run: |
set -euo pipefail
test "$(uname -m)" = x86_64
if [ "$(id -u)" -eq 0 ]; then
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends fakeroot rpm
else
sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends fakeroot rpm
fi
command -v fakeroot
command -v dpkg-deb
command -v rpmbuild
- name: Test and build JAR, sources, DEB and RPM
run: ./gradlew clean build sourcesJar createAll --no-daemon --console=plain
- name: Attach Linux x86_64 files to draft release
env:
GITEA_API_URL: ${{ gitea.api_url }}
GITEA_REPOSITORY: ${{ gitea.repository }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
RELEASE_ID: ${{ needs.prepare.outputs.release_id }}
run: |
set -euo pipefail
shopt -s nullglob
files=(build/libs/*-all.jar build/libs/*-src.jar build/dist/*_amd64.deb build/dist/*.x86_64.rpm)
test "${#files[@]}" -eq 4
for file in "${files[@]}"; do
curl --fail --silent --show-error \
-H "Authorization: token ${GITEA_TOKEN}" \
-F "attachment=@${file}" \
"${GITEA_API_URL}/repos/${GITEA_REPOSITORY}/releases/${RELEASE_ID}/assets" > /dev/null
done
linux_arm64:
needs: prepare
runs-on: linux-arm64
timeout-minutes: 240
steps:
- name: Test and build ARM64 packages under QEMU
env:
GITEA_SHA: ${{ gitea.sha }}
run: |
set -euo pipefail
test "$(uname -m)" = aarch64
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates openjdk-21-jdk fakeroot rpm git curl
git clone --quiet https://git.briggen.dev/NilsBriggen/Logisim-Revolution.git source
cd source
git checkout --detach "$GITEA_SHA"
./gradlew clean createAll --no-daemon --console=plain
- name: Attach Linux ARM64 files to draft release
env:
GITEA_API_URL: ${{ gitea.api_url }}
GITEA_REPOSITORY: ${{ gitea.repository }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
RELEASE_ID: ${{ needs.prepare.outputs.release_id }}
run: |
set -euo pipefail
cd source
shopt -s nullglob
files=(build/dist/*_arm64.deb build/dist/*.aarch64.rpm)
test "${#files[@]}" -eq 2
for file in "${files[@]}"; do
curl --fail --silent --show-error \
-H "Authorization: token ${GITEA_TOKEN}" \
-F "attachment=@${file}" \
"${GITEA_API_URL}/repos/${GITEA_REPOSITORY}/releases/${RELEASE_ID}/assets" > /dev/null
done
collect_github:
needs: prepare
runs-on: ubuntu-latest
timeout-minutes: 210
steps:
- uses: https://gitea.com/actions/checkout@v4
- name: Collect successful Windows and Mac packages from GitHub
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GITEA_API_URL: ${{ gitea.api_url }}
GITEA_REPOSITORY: ${{ gitea.repository }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_SHA: ${{ gitea.sha }}
RELEASE_TAG: build-${{ gitea.run_id }}-attempt-${{ gitea.run_attempt }}
RELEASE_ID: ${{ needs.prepare.outputs.release_id }}
run: python3 scripts/ci/github_release.py collect
publish:
needs: [prepare, linux_amd64, linux_arm64, collect_github]
runs-on: ubuntu-latest
steps:
- uses: https://gitea.com/actions/checkout@v4
- name: Verify all assets and publish the release
env:
GITEA_API_URL: ${{ gitea.api_url }}
GITEA_REPOSITORY: ${{ gitea.repository }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_RUN_ID: ${{ gitea.run_id }}
GITEA_RUN_ATTEMPT: ${{ gitea.run_attempt }}
RELEASE_ID: ${{ needs.prepare.outputs.release_id }}
run: python3 scripts/ci/release.py publish
+28
View File
@@ -0,0 +1,28 @@
name: Translation report
on:
push:
branches: [main]
paths:
- 'src/main/resources/resources/logisim/strings/**/*.properties'
- 'src/main/resources/resources/logisim/settings.properties'
pull_request:
branches: [main]
paths:
- 'src/main/resources/resources/logisim/strings/**/*.properties'
- 'src/main/resources/resources/logisim/settings.properties'
permissions:
code: read
jobs:
translations:
runs-on: ubuntu-latest
steps:
- uses: https://gitea.com/actions/checkout@v4
- name: Check localization bundles
run: |
set -euo pipefail
python3 -m venv .translation-venv
.translation-venv/bin/pip install --quiet trans-tool==2.5.2 pysimple-log==0.0.5
PATH="$PWD/.translation-venv/bin:$PATH" python3 scripts/ci/check_translations.py
-93
View File
@@ -1,93 +0,0 @@
# This workflow will build a Java project with Gradle
# For more information see:
# https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle
name: "Build"
on:
push:
# main is left just for emergency cases.
branches: [ main ]
pull_request:
branches: [ main ]
env:
JDK_VERSION: 21
JDK_DISTRO: 'temurin'
jobs:
analyze_sources:
name: "Do we need to build the application?"
runs-on: ubuntu-latest
outputs:
# Export 'filter' step check result so next step can use it.
run_build: ${{ steps.filter.outputs.src }}
steps:
- name: "Install packages..."
run: sudo apt-get install sysvbanner
# https://github.com/marketplace/actions/checkout
- name: "Checkout sources"
uses: actions/checkout@v7
# https://github.com/marketplace/actions/paths-changes-filter
- name: "Look for changes in monitored locations"
uses: dorny/paths-filter@v4
id: filter
with:
filters: |
src:
- 'src/**/*.java'
- 'src/**/*.properties'
- '*.gradle*'
- 'gradle.properties'
- 'gradle/**/*.properties'
- 'build.gradle.kts'
- '.github/workflows/build.yml'
- name: "WILL BUILD STEP BE RUN?"
run: |
found="NO"
[[ ${{ steps.filter.outputs.src }} == 'true' ]] && found="YES"
echo "run_build=${found}" >> $GITHUB_OUTPUT
echo -e "\n****************************************\n"
banner "${found}"
echo -e "****************************************"
# Build step.
build:
name: "Gradle builder"
runs-on: ubuntu-latest
# Will run only if analyze_sources determined it is needed.
needs: analyze_sources
if: needs.analyze_sources.outputs.run_build == 'true'
steps:
- name: "Checkout sources"
uses: actions/checkout@v7
- name: Set up JDK ${{ env.JDK_VERSION }} ${{ env.JDK_DISTRO }}
uses: actions/setup-java@v6
with:
java-version: ${{ env.JDK_VERSION }}
distribution: ${{ env.JDK_DISTRO }}
# This is so Sonar has access to the full git history
fetch-depth: 0
- name: Cache SonarCloud packages
uses: actions/cache@v6
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: Cache Gradle packages
uses: actions/cache@v6
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle
- name: "Build with Gradle"
run: |
chmod +x gradlew
./gradlew build -x checkstyleMain -x checkstyleTest
- name: "Run SonarQube"
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: ([ -z "$SONAR_TOKEN" ] || ./gradlew sonar)
-240
View File
@@ -1,240 +0,0 @@
# Ensures every pull request that touches the code also documents itself in
# CHANGES.md, in the topmost (unreleased) section, and that the added entry
# credits its author with their GitHub handle.
#
# The check is skipped for PRs that touch neither the code nor the project
# infrastructure (i.e. docs or translations only), for Dependabot, and for PRs
# explicitly marked as not needing an entry by putting NO_CHANGELOG_ENTRY in
# the PR description.
#
# The author credit part is waived by NO_CHANGELOG_AUTHOR_CREDIT, either put
# in the PR description (waives it for that PR only), or set as a repository
# variable (Settings -> Secrets and variables -> Actions -> Variables), which
# waives it repository wide with no need to touch this workflow. The entry
# itself is still required either way.
#
# Marcin Orlowski
name: "Changelog"
on:
pull_request:
branches: [ main ]
# "edited" so adding NO_CHANGELOG_* to the description re-runs the check.
types: [ opened, synchronize, reopened, edited ]
permissions:
contents: read
env:
# Set this repository variable to any value but "false"/"no"/"0" to stop
# requiring changelog entries to credit their author. Unset (the default)
# means the credit is required.
NO_CHANGELOG_AUTHOR_CREDIT: ${{ vars.NO_CHANGELOG_AUTHOR_CREDIT }}
jobs:
changelog:
name: "CHANGES.md entry"
runs-on: ubuntu-latest
steps:
# https://github.com/marketplace/actions/checkout
- name: "Checkout sources"
uses: actions/checkout@v7
with:
# Full history, as we diff the PR against its base commit.
fetch-depth: 0
- name: "Is the check opted out of?"
id: optout
env:
PR_BODY: ${{ github.event.pull_request.body }}
PR_USER: ${{ github.event.pull_request.user.login }}
run: |
skip="false"
reason=""
if grep -qF 'NO_CHANGELOG_ENTRY' <<< "${PR_BODY}"; then
skip="true"
reason="PR description contains NO_CHANGELOG_ENTRY"
elif [[ "${PR_USER}" == "dependabot[bot]" ]]; then
skip="true"
reason="PR comes from Dependabot"
fi
# Waives the author credit requirement for this PR only. The entry
# itself is still required.
skip_credit="false"
if grep -qF 'NO_CHANGELOG_AUTHOR_CREDIT' <<< "${PR_BODY}"; then
skip_credit="true"
fi
{
echo "skip=${skip}"
echo "skip_credit=${skip_credit}"
} >> "${GITHUB_OUTPUT}"
[[ "${skip}" == "true" ]] && echo "Skipping: ${reason}."
exit 0
# https://github.com/marketplace/actions/paths-changes-filter
- name: "Look for changes that matters for us…"
uses: dorny/paths-filter@v4
id: filter
if: steps.optout.outputs.skip == 'false'
with:
# Needed by the negated ("!") patterns below. Without it, they would be
# treated as ordinary patterns and match everything they exclude.
predicate-quantifier: 'some-with-excludes'
filters: |
code:
- added|modified|deleted: 'src/**'
- added|modified|deleted: 'buildSrc/**'
- added|modified|deleted: 'build.gradle*'
- added|modified|deleted: 'settings.gradle*'
- added|modified|deleted: 'gradle/libs.versions.toml'
- added|modified|deleted: '.github/**'
- added|modified|deleted: 'support/**'
- added|modified|deleted: 'snap/**'
# Documentation and translations need no changelog entry, even
# though they live under src/**.
- '!src/main/resources/doc/**'
- '!src/main/resources/resources/logisim/strings/**'
# NOTE: do not name this filter "changes", as that name is already
# taken by paths-filter's own output listing all matched filters.
changelog:
- added|modified: 'CHANGES.md'
- name: "Verify CHANGES.md was updated"
if: steps.optout.outputs.skip == 'false' && steps.filter.outputs.code == 'true'
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
CHANGES_TOUCHED: ${{ steps.filter.outputs.changelog }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
SKIP_CREDIT: ${{ steps.optout.outputs.skip_credit }}
run: |
set -euo pipefail
declare -r CHANGELOG="CHANGES.md"
fail() {
echo "*********************************************************"
echo "* FIXME! ${1}"
shift
for msg in "${@}"; do
echo "* ${msg}"
done
echo "*"
echo "* If the change really needs no entry (typo fix, internal"
echo "* cleanup, etc.), put NO_CHANGELOG_ENTRY in the PR description."
echo "* If unsure, add changelog entry. That's the most common case."
echo "*********************************************************"
exit 1
}
# Bails out on a missing or misplaced changelog entry.
fail_entry() {
fail "${1}" \
"" \
"This pull request changes the code, so it is expected to" \
"describe the change in the topmost (@dev) section of" \
"${CHANGELOG}, i.e.:" \
"" \
" * @dev (????-??-??)" \
" * Fixed the frobnicator not working (@${PR_AUTHOR})."
}
if [[ "${CHANGES_TOUCHED}" != "true" ]]; then
fail_entry "No ${CHANGELOG} entry found in this pull request."
fi
# Bounds of the topmost (unreleased) section of the changelog. Sections
# are top level list items, i.e. "* @dev (????-??-??)", "* v4.1.0 (...)".
section_start="$(grep -n -m 1 '^\* ' "${CHANGELOG}" | cut -d: -f1 || true)"
if [[ -z "${section_start}" ]]; then
fail_entry "Cannot find any release section in ${CHANGELOG}."
fi
section_end="$(awk -v start="${section_start}" \
'NR > start && /^\* / { print NR - 1; exit }' "${CHANGELOG}")"
# No further section? Then the unreleased one spans up to the last line.
[[ -z "${section_end}" ]] && section_end="$(wc -l < "${CHANGELOG}")"
echo "Unreleased section spans lines ${section_start}-${section_end} of ${CHANGELOG}:"
sed -n "${section_start}p" "${CHANGELOG}"
# Line numbers (in the PR's version of the file) of all non-blank lines
# this PR adds to the changelog.
added_lines="$(git diff --unified=0 "${BASE_SHA}" -- "${CHANGELOG}" | awk '
/^@@/ {
# "@@ -old,cnt +new,cnt @@" - we only care about the new file side.
match($0, /\+[0-9]+/)
line = substr($0, RSTART + 1, RLENGTH - 1) + 0
next
}
/^\+\+\+/ { next }
/^\+/ {
if ($0 !~ /^\+[[:space:]]*$/) print line
line++
next
}
/^[^-\\]/ { line++ }
')"
if [[ -z "${added_lines}" ]]; then
fail_entry "${CHANGELOG} is touched, but no new entry text was added."
fi
# Of the added lines, these are the ones in the unreleased section.
entry_lines=()
for line in ${added_lines}; do
if [[ "${line}" -ge "${section_start}" && "${line}" -le "${section_end}" ]]; then
entry_lines+=("${line}")
fi
done
if [[ "${#entry_lines[@]}" -eq 0 ]]; then
fail_entry "${CHANGELOG} was modified, but not in the topmost (unreleased) section."
fi
echo "Changelog entry added:"
for line in "${entry_lines[@]}"; do
sed -n "${line}p" "${CHANGELOG}"
done
if [[ "${SKIP_CREDIT}" == "true" ]]; then
echo "Author credit waived by the PR description."
exit 0
fi
# Being a "NO_..." switch, any value but the negative ones waives it.
case "${NO_CHANGELOG_AUTHOR_CREDIT,,}" in
"" | "false" | "no" | "0")
# credit required
;;
*)
echo "Author credit waived repository wide."
exit 0
;;
esac
# Entries are expected to credit their author, so look for the PR author's
# handle in the very lines this PR adds. GitHub logins are case insensitive
# and made of [A-Za-z0-9-] only
for line in "${entry_lines[@]}"; do
if sed -n "${line}p" "${CHANGELOG}" | grep -qiE "@${PR_AUTHOR}([^A-Za-z0-9-]|$)"; then
echo "Entry credits @${PR_AUTHOR}."
exit 0
fi
done
fail "Your ${CHANGELOG} entry does not credit changes author." \
"" \
"Entries are expected to mention the GitHub handle of the pull" \
"request author, so contributors get credited, i.e.:" \
"" \
" * Fixed the frobnicator not working (@${PR_AUTHOR})." \
"" \
"Please add \"@${PR_AUTHOR}\" to the entry you added to the @dev" \
"section, or put NO_CHANGELOG_AUTHOR_CREDIT in the PR description" \
"if you prefer not to be credited."
-118
View File
@@ -1,118 +0,0 @@
# This workflow will check code style of the project files (mostly Java).
#
# By design, it is expected to only analyse files affected by given
# pull request, and not the whole source tree.
#
# Marcin Orlowski
name: "Code style"
on:
push:
# main is left just for emergency cases.
branches: [ main ]
pull_request:
branches: [ main ]
env:
JDK_VERSION: 21
JDK_DISTRO: 'temurin'
jobs:
analyze_sources:
name: "Is there any Java source code to lint?"
runs-on: ubuntu-latest
outputs:
# Export 'filter' step check result so next step can use it.
run_checkstyle: ${{ steps.filter.outputs.src }}
# These are source files matching our filter that are affected by the PR.
changed_files: ${{ steps.filter.outputs.src_files }}
steps:
- name: "Install packages..."
run: sudo apt-get install -y sysvbanner
# https://github.com/marketplace/actions/checkout
- name: "Checkout sources"
uses: actions/checkout@v7
# https://github.com/marketplace/actions/paths-changes-filter
- name: "Look for changes that matters for us..."
uses: dorny/paths-filter@v4
id: filter
with:
list-files: 'escape'
filters: |
src:
- added|modified: 'src/**/*.java'
- name: "WILL JAVA LINT STEP BE RUN?"
run: |
found="NO"
[[ ${{ steps.filter.outputs.src }} == 'true' ]] && found="YES"
echo "run_checkstyle=${found}" >> $GITHUB_OUTPUT
echo -e "\n****************************************\n"
banner "${found}"
echo -e "****************************************"
# Build step.
checkstyle:
name: "Code style linter"
runs-on: ubuntu-latest
# Will run only if analyze_sources determined it is needed.
needs: analyze_sources
if: needs.analyze_sources.outputs.run_checkstyle == 'true'
# TODO: we may want to use checkstyle.jar directly, skipping cli wrapper
# that would let us use most recent version, but would also require downloading
# such instead of installing packaged version via apt.
steps:
- name: "Checkout sources"
uses: actions/checkout@v7
- name: Set up JDK ${{ env.JDK_VERSION }} ${{ env.JDK_DISTRO }}
uses: actions/setup-java@v6
with:
java-version: ${{ env.JDK_VERSION }}
distribution: ${{ env.JDK_DISTRO }}
- name: "Lint changed files"
run: |
# Run CheckStyle
# https://github.com/checkstyle/checkstyle/releases/
# Version should match the version in build.gradle.kts.
CHECKSTYLE_VERSION="13.9.0"
JAR="checkstyle-${CHECKSTYLE_VERSION}-all.jar"
URL="https://github.com/checkstyle/checkstyle/releases/download/checkstyle-${CHECKSTYLE_VERSION}/${JAR}"
echo "Downloading ${JAR}..."
wget --quiet "${URL}"
# Using built-in config.
declare -r CHECKS_FILE="/google_checks.xml"
declare -r REPORT_FILE="output.txt"
# Let's lint eventually...
echo "*********************************************************"
java -version
echo "Linting using $(java -jar "${JAR}" --version)"
java -jar "${JAR}" -o "${REPORT_FILE}" -c "${CHECKS_FILE}" ${{ needs.analyze_sources.outputs.changed_files }}
# Plain output always contains "Starting audit..." and "Audit done." lines. Let's remove them.
sed -i 's/^Starting audit...$//; s/^Audit done.$//' "${REPORT_FILE}"
# The `strings` tool is used to filter out empty lines.
cnt="$(strings "${REPORT_FILE}" | wc -l)"
if [[ "${cnt}" -gt 0 ]]; then
echo "*********************************************************"
echo "* FIXME! Code style guide violations detected: ${cnt}"
echo "*"
echo "* For more information about used code style guide see:"
echo "* https://github.com/logisim-evolution/logisim-evolution/blob/main/docs/style.md"
echo "*********************************************************"
cat "${REPORT_FILE}"
exit 1
else
echo "Looks good. No style guide violations found."
fi
-114
View File
@@ -1,114 +0,0 @@
name: "CodeQL"
on:
push:
branches: [ 'main' ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ 'main' ]
schedule:
- cron: '25 17 * * 6'
jobs:
analyze_sources:
name: "Is there any code to analyze?"
runs-on: ubuntu-latest
outputs:
run_analysis: ${{ steps.decide.outputs.run_analysis }}
steps:
# https://github.com/marketplace/actions/checkout
- name: "Checkout sources"
uses: actions/checkout@v7
# https://github.com/marketplace/actions/paths-changes-filter
- name: "Look for changes that matters for us…"
uses: dorny/paths-filter@v4
id: filter
if: github.event_name == 'pull_request'
with:
filters: |
src:
- added|modified|deleted: 'src/**/*.java'
- added|modified|deleted: 'buildSrc/**'
- added|modified|deleted: 'build.gradle*'
- added|modified|deleted: 'settings.gradle*'
- added|modified|deleted: 'gradle/**'
- name: "WILL CODEQL ANALYSIS BE RUN?"
id: decide
env:
EVENT_NAME: ${{ github.event_name }}
SRC_CHANGED: ${{ steps.filter.outputs.src }}
run: |
# Only pull requests are filtered. Pushes to main and scheduled scans
# must always run, or the code scanning results would go stale.
run_analysis="true"
if [[ "${EVENT_NAME}" == "pull_request" && "${SRC_CHANGED}" != "true" ]]; then
run_analysis="false"
fi
echo "run_analysis=${run_analysis}" >> "${GITHUB_OUTPUT}"
echo -e "\n****************************************"
echo "Event: ${EVENT_NAME}, code changed: ${SRC_CHANGED:-n/a} => ${run_analysis}"
echo -e "****************************************"
analyze:
name: Analyze
needs: analyze_sources
if: needs.analyze_sources.outputs.run_analysis == 'true'
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'java' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Use only 'java' to analyze code written in Java, Kotlin or both
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v7
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4.38.1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v4.38.1
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4.38.1
with:
category: "/language:${{matrix.language}}"
-55
View File
@@ -1,55 +0,0 @@
#
# Logisim-evolution PR/Issue cross dependency checker
# https://github.com/logisim-evolution/logisim-evolution
#
# Github Action that looks for "DEPENDS_ON <ID>" and "BLOCKED_BY <ID>"
# in PR description and checks state of referenced PRs ensure these
# are merged first.
#
# Marcin Orlowski
#
name: Dependabot
on:
issues:
types:
- opened
- edited
- reopened
pull_request_target:
types:
- opened
- edited
- reopened
# Makes sure we always add status check for PRs. Useful only if
# this action is required to pass before merging. Can be removed
# otherwise.
- synchronize
# Schedule a daily check. Useful if you reference cross-repository
# issues or pull requests. Can be removed otherwise.
schedule:
# m h dom mon dow
- cron: '0 0 * * *'
jobs:
check:
runs-on: ubuntu-latest
steps:
# https://github.com/marketplace/actions/dependent-issues
- uses: z0al/dependent-issues@v1
env:
# (Required) The token to use to make API calls to GitHub.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# (Optional) The token to use to make API calls to GitHub for remote repos.
# GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
with:
# (Optional) The label to use to mark dependent issues
label: dependent
# (Optional) Enable checking for dependencies in issues.
# Enable by setting the value to "on". Default "off"
check_issues: off
# (Optional) A comma-separated list of keywords.
keywords: DEPENDS_ON, BLOCKED_BY
-38
View File
@@ -1,38 +0,0 @@
# Runs markdownlint on all *.md files
name: "MD Lint"
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
markdown_lint:
name: "Markdown linter"
runs-on: ubuntu-latest
steps:
# https://github.com/marketplace/actions/checkout
- name: "Checkout sources"
uses: actions/checkout@v7
# https://github.com/marketplace/actions/paths-changes-filter
- name: "Look for changed doc related files..."
uses: dorny/paths-filter@v4
id: filter
with:
filters: |
docs:
- 'docs/**'
- '**/*.md'
# https://github.com/marketplace/actions/my-markdown-linter
- name: "Running markdown linter..."
uses: ruzickap/action-my-markdown-linter@v1
if: steps.filter.outputs.docs == 'true'
with:
# LICENSE is externally sourced and we're not going to fix it.
exclude: "LICENSE.md"
config_file: .markdownlint.yaml
-428
View File
@@ -1,428 +0,0 @@
#
# Logisim-evolution nightly builder
# https://github.com/logisim-evolution/logisim-evolution
#
# Cron driven Github Action builder, intended to create nightly builds
# from Logisim-evolution's "main" branch. It assumes to be invoked
# daily (once per 24 hrs) and skips building if there was no repo activity
# during last 24 hours.
#
# Marcin Orlowski
#
name: "Nightly"
on:
# To enable "nightly" on closing pull requests uncomment the following:
#pull_request:
# branches: [ main ]
# types: [ closed ]
schedule:
# every day at midnight
- cron: "0 0 * * *"
env:
JDK_VERSION: 21
JDK_DISTRO: 'temurin'
jobs:
git_check:
name: "Any changes since last run?"
runs-on: ubuntu-26.04
# Export build vars so other steps can use it.
outputs:
# Shall we build anything?
run_build: ${{ steps.filter.outputs.run_build }}
# Base application name and version (read from gradle.properties).
lse_name: ${{ steps.filter.outputs.lse_name }}
lse_version: ${{ steps.filter.outputs.lse_version }}
# Same as lse_version but with with no "unstable suffix (i.e. no "-dev" etc)
lse_version_short: ${{ steps.filter.outputs.lse_version_short }}
# Base name for final daily build files.
base_name: ${{ steps.filter.outputs.base_name }}
steps:
# https://github.com/marketplace/actions/checkout
- name: 'Checkout sources'
uses: actions/checkout@v7
# Check if latest commit is less than a day.
- name: "ANY CHANGES SINCE LAST BUILD?"
id: filter
continue-on-error: true
# if: ${{ github.event_name == 'schedule' }}
run: |
# Have no mercy.
set -euo pipefail
# Short name for branch in which action was triggered.
declare -r branch_name="${GITHUB_BASE_REF:-${GITHUB_REF#refs/heads/}}"
echo "Triggered for branch: ${branch_name}"
# Get SHA of last commit in that branch.
declare -r last_commit_sha="$(git rev-parse HEAD)"
# Let's see if we have any commits in last 24 hours.
if [[ -n "$(git rev-list --after="24 hours" ${last_commit_sha})" ]]; then
echo "run_build=true" >> $GITHUB_OUTPUT
else
echo "*** No code changes since yesterday. Skipping."
fi
# I need to export current version, even if is incorrect for main branch as it
# is part of produced artifacts. We will strip it prior uploading anyway.
# LSe version string can have additional "unstable" indicator (i.e. "-dev") but
# we are forced to drop the "-" as some packages do not allow it. So in fact
# Gradle artifact for "1.2.3-dev" uses "1.2.3dev" in file names, so we need to
# consider that whlie looking for artifacts (i.e. for upload).
declare -r version_prop=$(grep '^version\s*=.*' gradle.properties | awk '{print $3}')
# Remove "-" (if present) from version string.
declare -r lse_version=$(echo "${version_prop}" | awk '{gsub("-","", $0); print}')
echo "lse_version=${lse_version}" >> $GITHUB_OUTPUT
# Version string with suffix stripped. Required for some packages/platforms.
declare -r lse_version_short=$(echo "${version_prop}" | awk '{ split($0, v, "-")} {print v[1]}')
echo "lse_version_short=${lse_version_short}" >> $GITHUB_OUTPUT
declare -r lse_name="$(fgrep name gradle.properties | awk '{print $3}')"
echo "lse_name=${lse_name}" >> $GITHUB_OUTPUT
# Base name for uploaded artifacts.
echo "base_name=${lse_name}_${branch_name}_$(date +%Y%m%d)" >> $GITHUB_OUTPUT
# ###############################################################################################
snap:
name: Snapcraft build ${{ matrix.arch }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-24.04
arch: amd64
- os: ubuntu-24.04-arm
arch: arm64
runs-on: ${{ matrix.os }}
# Will run only if git_check determined it is needed.
needs: git_check
if: needs.git_check.outputs.run_build == 'true'
steps:
# https://github.com/z0al/dependent-issues
- name: 'Checkout branch: ${{ env.GITHUB_REF }}'
uses: actions/checkout@v7
- name: 'Build Snap package'
# https://github.com/snapcore/action-build
uses: snapcore/action-build@v1
id: snapcraft
- name: Review the built snap
# https://github.com/snapcrafters/ci
uses: snapcrafters/ci/review-snap@main
if: success()
with:
snap: ${{ steps.snapcraft.outputs.snap }}
- name: 'Upload *.snap artifact.'
uses: actions/upload-artifact@v7
if: success()
with:
path: ${{ steps.snapcraft.outputs.snap }}
name: ${{ needs.git_check.outputs.base_name }}_${{ matrix.arch }}.snap
- name: 'Pushing *.snap to snapcraft.io'
# https://github.com/snapcore/action-publish
uses: snapcore/action-publish@v1
if: success()
env:
# Obtain Snapcraft.io store token:
# $ snapcraft export-login --snaps=logisim-evolution --acls package_access,package_push,package_update,package_release token.txt
# Then, open Github repository settings and add new secred named SNAPCRAFT_STORE_TOKEN with the value of token.txt
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_TOKEN }}
with:
snap: ${{ steps.snapcraft.outputs.snap }}
release: edge
# Building for Linux.
build_linux:
name: 'Linux build'
runs-on: ubuntu-26.04
# Will run only if git_check determined it is needed.
needs: git_check
if: needs.git_check.outputs.run_build == 'true'
steps:
# https://github.com/z0al/dependent-issues
- name: 'Checkout branch: ${{ env.GITHUB_REF }}'
uses: actions/checkout@v7
with:
ref: 'main'
- name: Set up JDK ${{ env.JDK_VERSION }} ${{ env.JDK_DISTRO }}
uses: actions/setup-java@v6
with:
java-version: ${{ env.JDK_VERSION }}
distribution: ${{ env.JDK_DISTRO }}
# ###########################################################################################
- name: 'Build binary JAR'
run: |
# Have no mercy.
set -euo pipefail
chmod +x gradlew
./gradlew shadowJar
# https://github.com/marketplace/actions/upload-a-build-artifact
- name: 'Upload binary JAR'
uses: actions/upload-artifact@v7
if: success()
with:
path: build/libs/${{ needs.git_check.outputs.lse_name }}-${{ needs.git_check.outputs.lse_version }}-all.jar
name: ${{ needs.git_check.outputs.base_name }}-all-jdk${{ env.JDK_VERSION }}.jar
# ###########################################################################################
- name: 'Build DEB'
run: |
# Have no mercy.
set -euo pipefail
chmod +x gradlew
./gradlew createDeb -x checkstyleMain -x checkstyleTest
- name: 'Upload *.deb'
uses: actions/upload-artifact@v7
if: success()
with:
path: build/dist/${{ needs.git_check.outputs.lse_name }}_${{ needs.git_check.outputs.lse_version }}_amd64.deb
name: ${{ needs.git_check.outputs.base_name }}_amd64.deb
# ###########################################################################################
- name: 'Build RPM'
run: |
# Have no mercy.
set -euo pipefail
chmod +x gradlew
# Needs rpm package, but it should be installed already.
./gradlew createRpm -x checkstyleMain -x checkstyleTest
- name: 'Upload *.rpm'
uses: actions/upload-artifact@v7
if: success()
with:
path: build/dist/${{ needs.git_check.outputs.lse_name }}-${{ needs.git_check.outputs.lse_version }}-1.x86_64.rpm
name: ${{ needs.git_check.outputs.base_name }}.x86_64.rpm
# ###########################################################################################
- name: 'Build sources JAR'
run: |
# Have no mercy.
set -euo pipefail
chmod +x gradlew
# Need to clean to have just one JAR or createDistDir task would fail.
./gradlew clean
./gradlew sourcesJar
- name: 'Upload sources JAR'
uses: actions/upload-artifact@v7
if: success()
with:
path: build/libs/${{ needs.git_check.outputs.lse_name }}-${{ needs.git_check.outputs.lse_version }}-src.jar
name: ${{ needs.git_check.outputs.base_name }}-src.jar
# ###############################################################################################
# Building for Linux Arm.
build_linux_arm:
name: 'Linux Arm build'
runs-on: ubuntu-26.04-arm
# Will run only if git_check determined it is needed.
needs: git_check
if: needs.git_check.outputs.run_build == 'true'
steps:
# https://github.com/z0al/dependent-issues
- name: 'Checkout branch: ${{ env.GITHUB_REF }}'
uses: actions/checkout@v7
with:
ref: 'main'
- name: Set up JDK ${{ env.JDK_VERSION }} ${{ env.JDK_DISTRO }}
uses: actions/setup-java@v6
with:
java-version: ${{ env.JDK_VERSION }}
distribution: ${{ env.JDK_DISTRO }}
# ###########################################################################################
- name: 'Build Arm DEB'
run: |
# Have no mercy.
set -euo pipefail
chmod +x gradlew
./gradlew createDeb -x checkstyleMain -x checkstyleTest
- name: 'Upload *arm64.deb'
uses: actions/upload-artifact@v7
if: success()
with:
path: build/dist/${{ needs.git_check.outputs.lse_name }}_${{ needs.git_check.outputs.lse_version }}_arm64.deb
name: ${{ needs.git_check.outputs.base_name }}_arm64.deb
# ###########################################################################################
- name: 'Build Arm RPM'
run: |
# Have no mercy.
set -euo pipefail
chmod +x gradlew
# Needs rpm package, but it should be installed already.
./gradlew createRpm -x checkstyleMain -x checkstyleTest
- name: 'Upload *aarch64.rpm'
uses: actions/upload-artifact@v7
if: success()
with:
path: build/dist/${{ needs.git_check.outputs.lse_name }}-${{ needs.git_check.outputs.lse_version }}-1.aarch64.rpm
name: ${{ needs.git_check.outputs.base_name }}.aarch64.rpm
# ###############################################################################################
# Building for macOS.
build_macos:
name: 'macOS build'
runs-on: macos-latest
# Will run only if git_check determined it is needed.
needs: git_check
if: needs.git_check.outputs.run_build == 'true'
steps:
# https://github.com/marketplace/actions/checkout
- name: "Checkout sources"
uses: actions/checkout@v7
with:
ref: ${{ github.ref_name }}
- name: Set up aarch64 JDK ${{ env.JDK_VERSION }} ${{ env.JDK_DISTRO }}
uses: actions/setup-java@v6
with:
java-version: ${{ env.JDK_VERSION }}
distribution: ${{ env.JDK_DISTRO }}
- name: "Build aarch64 DMG"
run: |
# Have no mercy.
set -euo pipefail
chmod +x gradlew
./gradlew createDmg -x checkstyleMain -x checkstyleTest
- name: 'Upload aarch64 DMG'
uses: actions/upload-artifact@v7
if: success()
with:
path: build/dist/${{ needs.git_check.outputs.lse_name }}-${{ needs.git_check.outputs.lse_version }}-aarch64.dmg
name: ${{ needs.git_check.outputs.base_name }}-aarch64.dmg
- name: Set up x86_64 JDK ${{ env.JDK_VERSION }} ${{ env.JDK_DISTRO }}
uses: actions/setup-java@v6
with:
java-version: ${{ env.JDK_VERSION }}
distribution: ${{ env.JDK_DISTRO }}
architecture: "x64"
- name: "Build x86_64 DMG"
run: |
# Have no mercy.
set -euo pipefail
chmod +x gradlew
./gradlew createDmg -x checkstyleMain -x checkstyleTest
- name: 'Upload x86_64 DMG'
uses: actions/upload-artifact@v7
if: success()
with:
path: build/dist/${{ needs.git_check.outputs.lse_name }}-${{ needs.git_check.outputs.lse_version }}-x86_64.dmg
name: ${{ needs.git_check.outputs.base_name }}-x86_64.dmg
# ###############################################################################################
# Building for Windows.
build_windows:
name: 'Windows build'
runs-on: windows-latest
# Will run only if git_check determined it is needed.
needs: git_check
if: needs.git_check.outputs.run_build == 'true'
steps:
# https://github.com/marketplace/actions/checkout
- name: "Checkout sources"
uses: actions/checkout@v7
- name: Set up JDK ${{ env.JDK_VERSION }} ${{ env.JDK_DISTRO }}
uses: actions/setup-java@v6
with:
java-version: ${{ env.JDK_VERSION }}
distribution: ${{ env.JDK_DISTRO }}
- name: "Build MSI"
run: .\gradlew.bat createMsi -x checkstyleMain -x checkstyleTest
- name: "Upload MSI"
uses: actions/upload-artifact@v7
if: success()
with:
# NOTE: Gradle builder creates MSI file that always uses short version format in file name.
path: build\dist\${{ needs.git_check.outputs.lse_name }}-${{ needs.git_check.outputs.lse_version_short }}-amd64.msi
name: ${{ needs.git_check.outputs.base_name }}-amd64.msi
- name: "Build self-contained archive for Windows"
run: .\gradlew.bat createWindowsPortableZip -x checkstyleMain -x checkstyleTest
- name: "Upload self-contained archive for Windows"
uses: actions/upload-artifact@v7
if: success()
with:
path: build\dist\${{ needs.git_check.outputs.lse_name }}-${{ needs.git_check.outputs.lse_version }}-windows-amd64.zip
name: ${{ needs.git_check.outputs.base_name }}-windows-amd64.zip
# ###############################################################################################
# This is currently disabled because JPackage uses the WiX Toolset which is not installed.
# # Building for Windows Arm.
# build_windows_arm:
# name: 'Windows Arm build'
# runs-on: windows-11-arm
# # Will run only if git_check determined it is needed.
# needs: git_check
# if: needs.git_check.outputs.run_build == 'true'
#
# steps:
# # https://github.com/marketplace/actions/checkout
# - name: "Checkout sources"
# uses: actions/checkout@v7
# - name: Set up JDK ${{ env.JDK_VERSION }} ${{ env.JDK_DISTRO }}
# uses: actions/setup-java@v6
# with:
# java-version: ${{ env.JDK_VERSION }}
# distribution: ${{ env.JDK_DISTRO }}
#
# - name: "Build Arm MSI"
# run: .\gradlew.bat createMsi -x checkstyleMain -x checkstyleTest
#
# - name: "Upload Arm MSI"
# uses: actions/upload-artifact@v7
# if: success()
# with:
# # NOTE: Gradle builder creates AArch64 file that always uses short version format in file name.
# path: build\dist\${{ needs.git_check.outputs.lse_name }}-${{ needs.git_check.outputs.lse_version_short }}-aarch64.msi
# name: ${{ needs.git_check.outputs.base_name }}-aarch64.msi
+100
View File
@@ -0,0 +1,100 @@
name: Hosted Windows and macOS packages
run-name: Gitea build ${{ inputs.release_tag }}
on:
workflow_dispatch:
inputs:
source_sha:
description: Commit mirrored from Gitea main
required: true
release_tag:
description: Unique Gitea draft release tag
required: true
permissions:
contents: read
jobs:
windows_amd64:
runs-on: windows-2022
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
architecture: x64
- name: Verify the mirrored commit and build MSI and portable ZIP
shell: pwsh
env:
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
if ($env:GITHUB_SHA -ne $env:SOURCE_SHA) { throw 'GitHub mirror is not at the requested Gitea commit.' }
if ($env:PROCESSOR_ARCHITECTURE -ne 'AMD64') { throw 'An x86_64 Windows runner is required.' }
$wix = 'C:\Program Files (x86)\WiX Toolset v3.14\bin'
if (!(Test-Path "$wix\candle.exe") -or !(Test-Path "$wix\light.exe")) {
throw 'WiX Toolset 3 is unavailable.'
}
$env:PATH = "$wix;$env:PATH"
.\gradlew.bat clean createAll --no-daemon --console=plain
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- uses: actions/upload-artifact@v4
with:
name: windows-amd64
path: |
build/dist/*.msi
build/dist/*.zip
if-no-files-found: error
retention-days: 1
compression-level: 0
macos_amd64:
runs-on: macos-15-intel
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
architecture: x64
- name: Verify the mirrored commit and build Intel DMG
env:
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
test "$GITHUB_SHA" = "$SOURCE_SHA"
test "$(uname -m)" = x86_64
./gradlew clean createDmg --no-daemon --console=plain
- uses: actions/upload-artifact@v4
with:
name: macos-amd64
path: build/dist/*-x86_64.dmg
if-no-files-found: error
retention-days: 1
compression-level: 0
macos_arm64:
runs-on: macos-15
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
architecture: aarch64
- name: Verify the mirrored commit and build Apple Silicon DMG
env:
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
test "$GITHUB_SHA" = "$SOURCE_SHA"
test "$(uname -m)" = arm64
./gradlew clean createDmg --no-daemon --console=plain
- uses: actions/upload-artifact@v4
with:
name: macos-arm64
path: build/dist/*-aarch64.dmg
if-no-files-found: error
retention-days: 1
compression-level: 0
-121
View File
@@ -1,121 +0,0 @@
# Locks the conversation of every merged pull request, and of the ticket(s) that
# pull request closed, so that follow-up talk starts a fresh ticket instead of
# landing in a thread nobody watches any more.
#
# Runs on "pull_request_target" and not on "pull_request", because this project
# takes its contributions as pull requests from forks, and those only get a
# read-only token on "pull_request", while locking needs write access. That is
# safe here, as this workflow never checks out nor runs the pull request's code.
# It only reads the pull request description, and does so through an environment
# variable, never by pasting it into the script.
#
# Marcin Orlowski
name: "Post-Merge Cleanup"
on:
pull_request_target:
types: [ closed ]
permissions:
contents: read
issues: write
pull-requests: write
jobs:
lock:
name: "Lock merged PR and its ticket(s)"
runs-on: ubuntu-latest
# "closed" fires for merged pull requests and for dropped ones alike, and
# there's nothing to clean up after the latter.
if: github.event.pull_request.merged == true
steps:
- name: "Lock the pull request conversation"
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
# A pull request is an issue as far as this endpoint is concerned.
if [[ "$(gh api "repos/${REPO}/issues/${PR}" --jq '.locked')" == "true" ]]; then
echo "PR #${PR} conversation is already locked."
exit 0
fi
gh api --silent -X PUT "repos/${REPO}/issues/${PR}/lock" -f "lock_reason=resolved"
echo "PR #${PR} conversation locked."
- name: "Lock the ticket(s) the pull request closed"
# Runs even if locking the pull request itself went wrong, as the two are
# unrelated.
if: always()
env:
GH_TOKEN: ${{ github.token }}
PR_BODY: ${{ github.event.pull_request.body }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
# GitHub closes a referenced ticket as part of the merge, but the event
# that started this run can get here first, so give it time to catch up.
declare -r ATTEMPTS=3
declare -r DELAY=5
# Formats kept in sync with ticket.yml, so that anything accepted there
# also gets locked here.
declare -r KEYWORDS='close[sd]?|fix(e[sd])?|resolve[sd]?'
declare -r URL="https://github\.com/${REPO//./\\.}/issues/"
mapfile -t tickets < <(
grep -oiE "(${KEYWORDS})[[:space:]]*:?[[:space:]]+(#|${URL})[0-9]+" <<< "${PR_BODY}" \
| grep -oE '[0-9]+$' \
| sort -un \
|| true
)
if [[ "${#tickets[@]}" -eq 0 ]]; then
echo "This pull request references no ticket. Nothing to lock."
exit 0
fi
echo "Referenced ticket(s): ${tickets[*]/#/#}"
for ticket in "${tickets[@]}"; do
for (( attempt = 1; attempt <= ATTEMPTS; attempt++ )); do
if ! issue="$(gh api "repos/${REPO}/issues/${ticket}" 2>/dev/null)"; then
echo "#${ticket}: no such ticket. Skipped."
continue 2
fi
if [[ "$(jq -r '.state' <<< "${issue}")" == "closed" ]]; then
break
fi
if (( attempt < ATTEMPTS )); then
echo "#${ticket}: still open. Waiting ${DELAY}s for the merge to close it…"
sleep "${DELAY}"
fi
done
# GitHub only closes referenced tickets for pull requests merged into
# the default branch, so an open one here is expected, not an error.
state="$(jq -r '.state' <<< "${issue}")"
if [[ "${state}" != "closed" ]]; then
echo "#${ticket}: ${state}, so not locking it."
continue
fi
if [[ "$(jq -r '.locked' <<< "${issue}")" == "true" ]]; then
echo "#${ticket}: already locked."
continue
fi
if gh api --silent -X PUT "repos/${REPO}/issues/${ticket}/lock" -f "lock_reason=resolved"; then
echo "#${ticket}: locked."
else
echo "#${ticket}: could not be locked."
fi
done
-140
View File
@@ -1,140 +0,0 @@
# Ensures every pull request links the ticket it addresses, using one of GitHub's
# closing keywords in the PR description, i.e. "Closes #1234". Each referenced
# ticket must exist in this repository, must be an issue (tickets and PR share
# the same space in GitHub), and must still be in OPEN state.
#
# The check is skipped for Dependabot automatically.
#
# Any PR can opt-out from this check by adding `NO_TICKET` to the PR description.
#
# Marcin Orlowski
name: "Ticket"
on:
pull_request:
branches: [ main ]
# "edited" so adding the reference (or `NO_TICKET`) to the description
# re-runs the check.
types: [ opened, synchronize, reopened, edited ]
permissions:
contents: read
issues: read
jobs:
ticket:
name: "Check ticket reference"
runs-on: ubuntu-latest
steps:
- name: "Is the check opted out of?"
id: optout
env:
PR_BODY: ${{ github.event.pull_request.body }}
PR_USER: ${{ github.event.pull_request.user.login }}
run: |
skip="false"
reason=""
if grep -qF 'NO_TICKET' <<< "${PR_BODY}"; then
skip="true"
reason="PR description contains NO_TICKET"
elif [[ "${PR_USER}" == "dependabot[bot]" ]]; then
skip="true"
reason="PR comes from Dependabot"
fi
echo "skip=${skip}" >> "${GITHUB_OUTPUT}"
[[ "${skip}" == "true" ]] && echo "Skipping: ${reason}."
exit 0
- name: "Verify the PR references an open ticket"
if: steps.optout.outputs.skip == 'false'
env:
GH_TOKEN: ${{ github.token }}
PR_BODY: ${{ github.event.pull_request.body }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
fail() {
echo "*********************************************************"
echo "* FIXME! ${1}"
shift
for msg in "${@}"; do
echo "* ${msg}"
done
echo "*"
echo "* If this pull request **really** has no ticket behind it, put"
echo "* NO_TICKET in the PR description to opt-out from this check."
echo "* If unsure, open a ticket for this effort first."
echo "*********************************************************"
exit 1
}
# Bails out on a missing or unusable ticket reference.
fail_ref() {
fail "${@}" \
"" \
"Pull requests are expected to state the ticket they address," \
"so merging them closes it, i.e.:" \
"" \
" Closes #1234" \
"" \
"Any of GitHub's closing keywords will do (closes, fixes," \
"resolves, ...), and so will the full ticket URL in place of" \
"the #1234 shorthand."
}
# GitHub's closing keywords, see
# https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue
declare -r KEYWORDS='close[sd]?|fix(e[sd])?|resolve[sd]?'
# The long form of a reference, i.e. the ticket's own URL. Dots are the
# only regex metacharacter a repository slug can bring in.
declare -r URL="https://github\.com/${REPO//./\\.}/issues/"
# Ticket numbers referenced in the description, deduplicated. Both the
# "#1234" shorthand and the full URL are accepted, as GitHub itself
# honours either.
mapfile -t tickets < <(
grep -oiE "(${KEYWORDS})[[:space:]]*:?[[:space:]]+(#|${URL})[0-9]+" <<< "${PR_BODY}" \
| grep -oE '[0-9]+$' \
| sort -un \
|| true
)
if [[ "${#tickets[@]}" -eq 0 ]]; then
fail_ref "No ticket reference found in this pull request's description."
fi
echo "Referenced ticket(s): ${tickets[*]/#/#}"
for ticket in "${tickets[@]}"; do
if ! issue="$(gh api "repos/${REPO}/issues/${ticket}" 2>/dev/null)"; then
fail_ref "Ticket #${ticket} does not exist in ${REPO}." \
"" \
"https://github.com/${REPO}/issues/${ticket} (404 - Not Found)"
fi
# The issues endpoint serves pull requests too, and those carry a
# "pull_request" key that plain issues do not have.
if [[ "$(jq -r 'has("pull_request")' <<< "${issue}")" == "true" ]]; then
fail_ref "#${ticket} is a pull request, not a ticket." \
"" \
"https://github.com/${REPO}/pull/${ticket}"
fi
state="$(jq -r '.state' <<< "${issue}")"
if [[ "${state}" != "open" ]]; then
fail_ref "Ticket #${ticket} is ${state}, not open." \
"" \
"$(jq -r '.title' <<< "${issue}")" \
"https://github.com/${REPO}/issues/${ticket}" \
"" \
"Reopen it, or reference the ticket this work really belongs to."
fi
echo " #${ticket} is an open ticket: $(jq -r '.title' <<< "${issue}")"
done
-78
View File
@@ -1,78 +0,0 @@
#
# Logisim-evolution translation files (*.properties) checker.
#
# Github action that ensures project translation files are in sync and
# in a good shape in general.
#
# Worker uses trans-tool: https://github.com/MarcinOrlowski/trans-tool
#
# Marcin Orlowski
#
name: "Translations"
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
translations_lint:
name: "Translations linter"
runs-on: ubuntu-latest
steps:
# https://github.com/marketplace/actions/checkout
- name: "Checkout sources"
uses: actions/checkout@v7
# https://github.com/marketplace/actions/paths-changes-filter
- name: "Looking for modified files..."
uses: dorny/paths-filter@v4
id: filter
with:
filters: |
translations:
- 'src/main/resources/resources/logisim/**/*.properties'
- name: "===> SEE HERE FOR LINTING REPORT"
if: steps.filter.outputs.translations == 'true'
run: |
# Have no mercy.
set -euo pipefail
sudo pip install pysimple-log==0.0.5 trans-tool==2.5.2
trans-tool --version
# Declare constants
declare -r resources_path="src/main/resources/resources/logisim/"
declare -r base_language="en"
# Get all Logisim's localization base (EN) files
declare -r base_files=$(find ${resources_path}* -name "*.properties" | grep -E '*/[a-zA-Z]+.properties$' | grep -v settings.properties)
# Read currently supported languages from settings.properties
declare -r langs="$(cut -d ' ' -f3- < "${resources_path}settings.properties")"
# Validate.
failed=
for file in ${base_files}; do
# Keep validating even if previous run failed, so we can have complete report.
trans-tool -l "${langs}" -ls "${base_language}" -b "${file}" >> /tmp/report.txt || failed="YES"
done
if [[ "${failed}" == "YES" ]]; then
echo ""
echo "Project *.properties files have some problems or are out of sync."
echo "Please see 'Update existing translations' in trans-tool's docs:"
echo " https://github.com/MarcinOrlowski/trans-tool"
echo "for more info on how to address these issues."
echo ""
cat /tmp/report.txt
echo ""
# Do not fail the action no matter what. This is intentional
# unless translation files are in better condition.
# exit 1
fi
+2
View File
@@ -296,3 +296,5 @@ gradle-app.setting
!/snap/local/usr/bin/
*.py
!/artwork/generate_revolution_assets.py
!/scripts/ci/*.py
__pycache__/
+7 -6
View File
@@ -12,13 +12,15 @@ Logisim-evolution is a Java 21 / Swing digital logic designer and simulator, bui
./gradlew test # run tests (JUnit 5 / junit-jupiter)
./gradlew test --tests com.cburch.logisim.std.ttl.Ttl7493Test # single test class
./gradlew checkstyleMain # lint main sources (checkstyleTest for tests)
./gradlew shadowJar # fat jar -> build/libs/logisim-evolution-<ver>-all.jar
./gradlew shadowJar # fat jar -> build/libs/logisim-revolution-<ver>-all.jar
./gradlew createAll # platform installer(s) into build/dist (jpackage; host platform only)
./gradlew genFiles # run code generation only (needed before importing into Eclipse)
```
CI (`.github/workflows/build.yml`) runs `./gradlew build -x checkstyleMain -x checkstyleTest` on JDK 21
(temurin); style is checked separately (`checkstyle.yml`), and only on files the PR touches.
Gitea CI (`.gitea/workflows/ci.yml`) runs `./gradlew build` on JDK 21, including tests and
Checkstyle. A manual Gitea workflow builds Linux packages and dispatches Windows and macOS
packages on the public GitHub mirror; it publishes a Gitea release only after all targets
succeed. See `.gitea/README.md`.
## Build-time code generation
@@ -76,12 +78,11 @@ prefixed `# ==> key =` mark untranslated keys.
- Code style is **Google Java Style** via Checkstyle (`google_checks.xml` shipped with the tool),
relaxed by `checkstyle-suppressions.xml` in the repo root — 2-space indent, and the codebase widely
uses the `final var` idiom. Keep the Checkstyle version in `build.gradle.kts` and
`.github/workflows/checkstyle.yml` in sync.
uses the `final var` idiom. The full Checkstyle task runs as part of Gitea CI.
- Every source file carries the project's GPLv3 header comment block.
- Files are UTF-8, LF line endings, no trailing whitespace, and end with a newline
(`.pre-commit-config.yaml.dist` enforces this; copy it to `.pre-commit-config.yaml` to use).
- **CI enforces two PR requirements** (`changelog.yml`, `ticket.yml`): an entry in the topmost `@dev`
- **CI enforces two PR requirements** (`scripts/ci/pr_checks.py`): an entry in the topmost `@dev`
section of `CHANGES.md` crediting the author (`* Fixed the frobnicator (@nick).`), and a closing
reference to an existing open issue (`Closes #1234`) in the PR description. Opt out with
`NO_CHANGELOG_ENTRY` / `NO_CHANGELOG_AUTHOR_CREDIT` / `NO_TICKET` in the description.
+2
View File
@@ -3,6 +3,8 @@
# Changes #
* @dev (????-??-??)
* Reworked Gitea CI and manual all-platform release builds, added complete native-package
publication checks, and documented realistic installation methods (@NilsBriggen).
* Gave Revolution its own application identity, loading splash, vector icon family, readable
component captions and isolated settings and recovery paths. Refreshed Help and Preferences,
and rewrote the README around the new interface and verified source-build path
+7 -6
View File
@@ -12,13 +12,15 @@ Logisim-evolution is a Java 21 / Swing digital logic designer and simulator, bui
./gradlew test # run tests (JUnit 5 / junit-jupiter)
./gradlew test --tests com.cburch.logisim.std.ttl.Ttl7493Test # single test class
./gradlew checkstyleMain # lint main sources (checkstyleTest for tests)
./gradlew shadowJar # fat jar -> build/libs/logisim-evolution-<ver>-all.jar
./gradlew shadowJar # fat jar -> build/libs/logisim-revolution-<ver>-all.jar
./gradlew createAll # platform installer(s) into build/dist (jpackage; host platform only)
./gradlew genFiles # run code generation only (needed before importing into Eclipse)
```
CI (`.github/workflows/build.yml`) runs `./gradlew build -x checkstyleMain -x checkstyleTest` on JDK 21
(temurin); style is checked separately (`checkstyle.yml`), and only on files the PR touches.
Gitea CI (`.gitea/workflows/ci.yml`) runs `./gradlew build` on JDK 21, including tests and
Checkstyle. A manual Gitea workflow builds Linux packages and dispatches Windows and macOS
packages on the public GitHub mirror; it publishes a Gitea release only after all targets
succeed. See `.gitea/README.md`.
## Build-time code generation
@@ -76,12 +78,11 @@ prefixed `# ==> key =` mark untranslated keys.
- Code style is **Google Java Style** via Checkstyle (`google_checks.xml` shipped with the tool),
relaxed by `checkstyle-suppressions.xml` in the repo root — 2-space indent, and the codebase widely
uses the `final var` idiom. Keep the Checkstyle version in `build.gradle.kts` and
`.github/workflows/checkstyle.yml` in sync.
uses the `final var` idiom. The full Checkstyle task runs as part of Gitea CI.
- Every source file carries the project's GPLv3 header comment block.
- Files are UTF-8, LF line endings, no trailing whitespace, and end with a newline
(`.pre-commit-config.yaml.dist` enforces this; copy it to `.pre-commit-config.yaml` to use).
- **CI enforces two PR requirements** (`changelog.yml`, `ticket.yml`): an entry in the topmost `@dev`
- **CI enforces two PR requirements** (`scripts/ci/pr_checks.py`): an entry in the topmost `@dev`
section of `CHANGES.md` crediting the author (`* Fixed the frobnicator (@nick).`), and a closing
reference to an existing open issue (`Closes #1234`) in the PR description. Opt out with
`NO_CHANGELOG_ENTRY` / `NO_CHANGELOG_AUTHOR_CREDIT` / `NO_TICKET` in the description.
+62 -19
View File
@@ -2,17 +2,52 @@
# Logisim Revolution
Design and simulate digital circuits in a local desktop app. Logisim Revolution combines a searchable parts picker, a tabbed circuit editor, an inspector for component settings, and built-in tools for testing and analyzing logic.
Design and simulate digital circuits in a local desktop app. Logisim Revolution combines a
searchable parts picker, a tabbed circuit editor, an inspector for component settings, and
built-in tools for testing and analyzing logic.
It is a fork of [Logisim-evolution](https://github.com/logisim-evolution/logisim-evolution). It keeps the familiar circuit file format and component IDs while developing its own interface and application identity.
It is a fork of [Logisim-evolution](https://github.com/logisim-evolution/logisim-evolution).
It keeps the familiar circuit file format and component IDs while developing its own interface
and application identity.
![Dark-theme editor showing the circuit canvas, parts picker, and inspector](docs/img/logisim-revolution-main-dark.png)
![Light-theme editor showing a selected gate and its settings beside the parts picker](docs/img/logisim-revolution-main-light.png)
## Try it from source
## Install
You need **JDK 21 or newer**. The Gradle wrapper is included; a separate Gradle installation is not needed.
Completed manual builds are published on the
[Gitea Releases page](https://git.briggen.dev/NilsBriggen/Logisim-Revolution/releases).
Choose the file for your operating system and CPU. A release appears only after all supported
native targets have built successfully; there may be no Revolution release yet.
| System | Download | Install or run |
| --- | --- | --- |
| Debian/Ubuntu, x86_64 or ARM64 | `.deb` | `sudo apt install ./logisim-revolution_*.deb` |
| Fedora/RHEL, x86_64 or ARM64 | `.rpm` | `sudo dnf install ./logisim-revolution-*.rpm` |
| Windows x86_64 | `.msi` | Open the installer and follow its prompts. |
| Windows x86_64, portable | `-windows-amd64.zip` | Extract the whole ZIP, then run `logisim-revolution.exe` inside it. |
| macOS Intel or Apple Silicon | `.dmg` | Open the disk image and copy Logisim Revolution to Applications. |
These native packages include a Java runtime. The development DMGs are not notarized, so macOS
may show a security warning on first launch. Snap and Flatpak files are not currently built or
published for Revolution.
### Portable JAR
The `-all.jar` release file runs on Linux, Windows, and macOS with **Java 21 or newer**.
Download it and run, for example:
```bash
java --enable-native-access=ALL-UNNAMED -jar logisim-revolution-5.1.0dev-all.jar
```
Use the actual downloaded filename when the version changes. The JAR does not include a Java runtime.
The `-src.jar` file contains source code; it is not the application launcher.
### Build from source
Install JDK 21 or newer, then use the included Gradle wrapper:
```bash
git clone https://git.briggen.dev/NilsBriggen/Logisim-Revolution.git
@@ -20,36 +55,41 @@ cd Logisim-Revolution
./gradlew run
```
On Windows, run `gradlew.bat run`. To build a portable JAR instead:
```bash
./gradlew shadowJar
java -jar build/libs/logisim-revolution-5.1.0dev-all.jar
```
The JAR filename follows the version in [gradle.properties](gradle.properties); adjust that command when the version changes. Platform installers can be built on their respective operating systems with `./gradlew createAll`. **No Revolution installers or package-store releases are advertised here until they are published and verified.**
On Windows, use `gradlew.bat run`. To build a portable JAR locally, run
`./gradlew shadowJar`; to build native packages for your current operating system, run
`./gradlew createAll` after installing its platform packaging tools. See the
[developer guide](docs/developers.md) and [Gitea runner setup](.gitea/README.md) for details.
## Make a first circuit
1. Choose **New** on the welcome screen. Use the parts picker search to find **Pin**, place two input pins, then add an **AND** gate and an **LED**.
2. Select the wiring tool and connect both pins to the gate inputs, then connect the gate output to the LED.
1. Choose **New** on the welcome screen. Use the parts picker search to find **Pin**, place two
input pins, then add an **AND** gate and an **LED**.
2. Select the wiring tool and connect both pins to the gate inputs, then connect the gate output
to the LED.
3. Use the poke tool to switch the inputs between 0 and 1. The LED lights when both inputs are 1.
4. Save your work as a `.circ` file. Select a component to change its settings in the inspector.
The [built-in help](src/main/resources/doc) and [project background](docs/docs.md) cover more concepts and examples.
The [built-in help](src/main/resources/doc) and [project background](docs/docs.md) cover more
concepts and examples.
## What you can build
- Combinational and sequential circuits, from small gates to reusable subcircuits.
- Signals and displays, memory, arithmetic, TTL components, and SoC designs.
- Truth tables, expression analysis, test vectors, timing diagrams, and signal logs.
- VHDL components and HDL export; board and FPGA workflows where supported by your hardware and toolchain.
- VHDL components and HDL export; board and FPGA workflows where supported by your hardware
and toolchain.
The editor follows the system light or dark theme by default. UI zoom can be changed independently of circuit zoom.
The editor follows the system light or dark theme by default. UI zoom can be changed
independently of circuit zoom.
## Files and settings
Revolution opens and saves the existing Logisim-evolution `.circ` format. Saved component and library identifiers are unchanged. On the first interactive launch, Revolution offers to copy compatible Evolution settings. Declining leaves Revolution on its own defaults. The two apps keep separate settings and recovery files; importing does not delete or modify Evolution's settings.
Revolution opens and saves the existing Logisim-evolution `.circ` format. Saved component and
library identifiers are unchanged. On the first interactive launch, Revolution offers to copy
compatible Evolution settings. Declining leaves Revolution on its own defaults. The two apps
keep separate settings and recovery files; importing does not delete or modify Evolution's
settings.
## Project and contributions
@@ -59,4 +99,7 @@ Revolution opens and saves the existing Logisim-evolution `.circ` format. Saved
- [Full project credits](docs/credits.md)
- [License: GNU GPL version 3](LICENSE.md)
Logisim was created by Carl Burch. Logisim-evolution and its contributors developed the project further; Revolution builds on that work. See the [credits](docs/credits.md) and source headers for attribution. The new Revolution emblem and wordmark are included under the project's GPLv3 license.
Logisim was created by Carl Burch. Logisim-evolution and its contributors developed the project
further; Revolution builds on that work. See the [credits](docs/credits.md) and source headers
for attribution. The new Revolution emblem and wordmark are included under the project's GPLv3
license.
+12 -10
View File
@@ -334,6 +334,9 @@ object func {
/** Helper function to remove all contents from the given directory */
fun deleteDirectoryContents(directory: String) {
val dir = File(directory)
if (!dir.exists() && !dir.mkdirs()) {
throw GradleException("Cannot create ${directory}")
}
if (!dir.isDirectory) {
throw GradleException("Cannot remove contents of ${directory}")
}
@@ -363,26 +366,25 @@ object func {
/**
* Helper function to verify the distribution file now exists in build/dist.
* It issues a warning if it does not and also lists the contents of its directory.
* It fails if the expected package is missing, and lists the actual directory contents.
*/
fun verifyFileExists(filename: String) {
val theFile = File(filename)
if (theFile.isFile()) {
return
}
logger.warn("*** WARNING ***");
logger.warn("File does not exist: ${filename}")
logger.error("File does not exist: ${filename}")
val parentDir = theFile.getParentFile();
if (parentDir != null && parentDir.isDirectory()) {
logger.warn("Directory actually contains:")
logger.error("Directory actually contains:")
val dirList = parentDir.list()
if (dirList == null) return;
for (file in dirList) {
logger.warn(" ${file}")
for (file in dirList.orEmpty()) {
logger.error(" ${file}")
}
} else {
logger.warn("Parent directory does not exist: ${parentDir}");
logger.error("Parent directory does not exist: ${parentDir}")
}
throw GradleException("Expected package was not created: ${filename}")
}
/**
@@ -669,7 +671,7 @@ tasks.register("createMsi") {
"--type", "msi",
// we MUST use short version form (without any suffix like "-dev", as it is not allowed in MSI package:
// https://docs.microsoft.com/en-us/windows/win32/msi/productversion?redirectedfrom=MSDN
// NOTE: any change to version **format** may require editing of .github/workflows/nightly.yml too!
// NOTE: release.py validates these package names before publishing.
"--app-version", version,
)
func.runCommand(params, "Error while creating the MSI package.")
@@ -722,7 +724,7 @@ tasks.register("createExe") {
"--type", "app-image",
// we MUST use short version form (without any suffix like "-dev", as it is not allowed in MSI package:
// https://docs.microsoft.com/en-us/windows/win32/msi/productversion?redirectedfrom=MSDN
// NOTE: any change to version **format** may require editing of .github/workflows/nightly.yml too!
// NOTE: release.py validates these package names before publishing.
"--app-version", version,
)
func.runCommand(params, "Error while creating the Windows executable.")
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env python3
"""Summarize the inherited translation warnings without flooding Actions logs."""
import re
import subprocess
from pathlib import Path
def main():
root = Path("src/main/resources/resources/logisim")
settings = (root / "settings.properties").read_text(encoding="utf-8")
locales = next(line.split("=", 1)[1].strip() for line in settings.splitlines() if line.startswith("locales"))
bundles = sorted(
base for directory in (root / "strings").iterdir()
if directory.is_dir() and (base := directory / f"{directory.name}.properties").is_file()
)
failures = []
for base in bundles:
result = subprocess.run(
["trans-tool", "-l", locales, "-ls", "en", "-b", str(base)],
capture_output=True,
text=True,
check=False,
)
if result.returncode:
failures.append((base, result.stdout + result.stderr))
print(f"Checked {len(bundles)} localization bundles; {len(failures)} reported existing issues.")
for base, report in failures:
first = re.sub(r"\x1b\[[0-9;]*m", "", report).splitlines()[:3]
print(f"{base}: {' | '.join(first)}")
if failures:
print("Translation lint remains advisory until inherited bundle issues are resolved.")
if __name__ == "__main__":
main()
+207
View File
@@ -0,0 +1,207 @@
#!/usr/bin/env python3
"""Dispatch hosted native builds and copy their artifacts into a Gitea draft."""
import json
import shutil
import uuid
import os
import subprocess
import sys
import tempfile
import time
import urllib.error
import urllib.request
import zipfile
from pathlib import Path
GITHUB_REPOSITORY = "NilsBriggen/Logisim-Revolution"
GITHUB_WORKFLOW = "platforms.yml"
ARTIFACTS = {
"windows-amd64": {".msi", ".zip"},
"macos-amd64": {"-x86_64.dmg"},
"macos-arm64": {"-aarch64.dmg"},
}
def github_api(path, method="GET", data=None):
request = urllib.request.Request(
f"https://api.github.com/repos/{GITHUB_REPOSITORY}/{path}",
data=json.dumps(data).encode() if data is not None else None,
headers={
"Authorization": f"Bearer {os.environ['GH_TOKEN']}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
"Content-Type": "application/json",
"User-Agent": "logisim-revolution-release",
},
method=method,
)
with urllib.request.urlopen(request, timeout=60) as response:
content = response.read()
return json.loads(content) if content else None
def git(*args, env=None):
subprocess.run(["git", *args], check=True, env=env)
def mirror_and_dispatch():
sha = os.environ["GITEA_SHA"]
if subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip() != sha:
raise ValueError("Gitea checkout does not match the dispatched commit.")
tag = os.environ["RELEASE_TAG"]
with tempfile.TemporaryDirectory() as temporary:
askpass = Path(temporary) / "askpass.sh"
askpass.write_text(
'#!/bin/sh\ncase "$1" in *Username*) printf "x-access-token";; '
'*Password*) printf "%s" "$GH_TOKEN";; *) exit 1;; esac\n',
encoding="utf-8",
)
askpass.chmod(0o700)
env = os.environ.copy()
env.update({"GIT_ASKPASS": str(askpass), "GIT_TERMINAL_PROMPT": "0"})
git(
"-c", "credential.helper=", "push",
f"https://github.com/{GITHUB_REPOSITORY}.git",
f"{sha}:refs/heads/main", env=env,
)
mirror_sha = github_api("git/ref/heads/main")["object"]["sha"]
if mirror_sha != sha:
raise ValueError(f"GitHub mirror is at {mirror_sha}, expected {sha}.")
github_api(
f"actions/workflows/{GITHUB_WORKFLOW}/dispatches", "POST",
{"ref": "main", "inputs": {"source_sha": sha, "release_tag": tag}},
)
print(f"Dispatched GitHub Windows and macOS builds for {tag} ({sha}).")
def matching_run(tag):
result = github_api(
f"actions/workflows/{GITHUB_WORKFLOW}/runs?event=workflow_dispatch&per_page=50"
)
matches = [
run for run in result["workflow_runs"]
if run["display_title"] == f"Gitea build {tag}"
and run["head_sha"] == os.environ["GITEA_SHA"]
]
return max(matches, key=lambda run: run["id"]) if matches else None
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, request, fp, code, message, headers, newurl):
return None
def download_artifact(url, destination):
request = urllib.request.Request(
url,
headers={
"Authorization": f"Bearer {os.environ['GH_TOKEN']}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": "logisim-revolution-release",
},
)
try:
urllib.request.build_opener(NoRedirect).open(request, timeout=60)
except urllib.error.HTTPError as response:
if response.code not in (301, 302, 303, 307, 308):
raise
location = response.headers["Location"]
else:
raise ValueError("GitHub artifact endpoint did not redirect to a download.")
if not location.startswith("https://"):
raise ValueError("Artifact download URL is not HTTPS.")
with urllib.request.urlopen(location, timeout=180) as response:
destination.write_bytes(response.read())
def valid_names(group, names):
if group == "windows-amd64":
return len(names) == 2 and {Path(name).suffix for name in names} == ARTIFACTS[group]
suffix = next(iter(ARTIFACTS[group]))
return len(names) == 1 and names[0].endswith(suffix)
def gitea_upload(release_id, path):
boundary = f"logisim-{uuid.uuid4().hex}"
start = (
f"--{boundary}\r\nContent-Disposition: form-data; "
f' name="attachment"; filename="{path.name}"\r\n'
"Content-Type: application/octet-stream\r\n\r\n"
).encode()
body = start + path.read_bytes() + f"\r\n--{boundary}--\r\n".encode()
api = os.environ["GITEA_API_URL"].rstrip("/")
repo = os.environ["GITEA_REPOSITORY"]
request = urllib.request.Request(
f"{api}/repos/{repo}/releases/{release_id}/assets",
data=body,
headers={
"Authorization": f"token {os.environ['GITEA_TOKEN']}",
"Content-Type": f"multipart/form-data; boundary={boundary}",
},
method="POST",
)
with urllib.request.urlopen(request, timeout=180) as response:
uploaded = json.load(response)
if uploaded["name"] != path.name or uploaded["size"] != path.stat().st_size:
raise ValueError(f"Gitea attachment verification failed for {path.name}.")
print(f"Attached {path.name} ({uploaded['size']} bytes).")
def collect():
tag = os.environ["RELEASE_TAG"]
deadline = time.monotonic() + 3 * 60 * 60
last_status = None
while time.monotonic() < deadline:
run = matching_run(tag)
if run and run["status"] == "completed":
if run["conclusion"] != "success":
raise ValueError(f"GitHub build failed: {run['html_url']}")
break
status = run["status"] if run else "waiting for dispatch"
if status != last_status:
print(f"GitHub build: {status}", flush=True)
last_status = status
time.sleep(30)
else:
raise ValueError(f"Timed out waiting for GitHub build {tag}.")
result = github_api(f"actions/runs/{run['id']}/artifacts?per_page=100")
artifacts = result["artifacts"]
by_name = {artifact["name"]: artifact for artifact in artifacts}
if len(artifacts) != len(ARTIFACTS) or set(by_name) != set(ARTIFACTS):
raise ValueError(f"Expected three GitHub artifacts, got {[a['name'] for a in artifacts]}.")
with tempfile.TemporaryDirectory() as temporary:
for group, artifact in by_name.items():
if artifact["expired"] or artifact["size_in_bytes"] <= 0:
raise ValueError(f"GitHub artifact {group} is empty or expired.")
archive = Path(temporary) / f"{group}.zip"
download_artifact(artifact["archive_download_url"], archive)
with zipfile.ZipFile(archive) as bundle:
names = bundle.namelist()
if not valid_names(group, names) or any(
Path(name).name != name or not name for name in names
):
raise ValueError(f"Unexpected files in GitHub artifact {group}: {names}")
for name in names:
path = Path(temporary) / name
with bundle.open(name) as source, path.open("wb") as output:
shutil.copyfileobj(source, output)
if path.stat().st_size <= 0:
raise ValueError(f"Empty release file: {name}")
gitea_upload(os.environ["RELEASE_ID"], path)
print(f"Collected all Windows and macOS files from {run['html_url']}.")
if __name__ == "__main__":
try:
if sys.argv[1:] == ["dispatch"]:
mirror_and_dispatch()
elif sys.argv[1:] == ["collect"]:
collect()
else:
raise ValueError("usage: github_release.py dispatch|collect")
except (ValueError, KeyError, OSError, subprocess.CalledProcessError, urllib.error.URLError) as error:
print(f"GitHub build handoff failed: {error}", file=sys.stderr)
sys.exit(1)
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env python3
"""Lint Markdown changed by the current Gitea push or pull request."""
import json
import os
import subprocess
from pathlib import Path
def main():
event = json.loads(Path(os.environ["GITEA_EVENT_PATH"]).read_text(encoding="utf-8"))
if os.environ["GITEA_EVENT_NAME"] == "pull_request":
base = event["pull_request"]["base"]["sha"]
revision_range = f"{base}...HEAD"
else:
base = event.get("before", "")
revision_range = f"{base}..HEAD" if base and set(base) != {"0"} else "HEAD^..HEAD"
files = subprocess.check_output(
["git", "diff", "--name-only", revision_range], text=True
).splitlines()
markdown = [
name for name in files
if name.endswith(".md") and name != "LICENSE.md"
and not name.startswith("docs/qa/") and Path(name).is_file()
]
if not markdown:
print("No maintained Markdown files changed.")
return
print("Linting:", ", ".join(markdown), flush=True)
subprocess.run(
["npx", "--yes", "markdownlint-cli@0.49.1", "--config", ".markdownlint.yaml", *markdown],
check=True,
)
if __name__ == "__main__":
main()
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""Lock a merged PR and any issues it closed in this Gitea repository."""
import json
import os
import time
from pathlib import Path
from pr_checks import api_request, ticket_numbers
def lock_if_closed(number):
repo = os.environ["GITEA_REPOSITORY"]
for attempt in range(3):
issue = api_request(f"repos/{repo}/issues/{number}")
if issue["state"] == "closed":
break
if attempt < 2:
time.sleep(5)
if issue["state"] != "closed":
print(f"#{number} is still open; leaving it unlocked.")
return
if issue["is_locked"]:
print(f"#{number} is already locked.")
return
api_request(
f"repos/{repo}/issues/{number}/lock",
method="PUT",
data={"lock_reason": "resolved"},
)
print(f"Locked #{number}.")
def main():
event = json.loads(Path(os.environ["GITEA_EVENT_PATH"]).read_text(encoding="utf-8"))
request = event["pull_request"]
if not request.get("merged"):
print("PR was closed without merging; nothing to lock.")
return
lock_if_closed(request["number"])
body = request.get("body") or ""
for number in ticket_numbers(body, os.environ["GITEA_SERVER_URL"], os.environ["GITEA_REPOSITORY"]):
lock_if_closed(number)
if __name__ == "__main__":
main()
+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""Gitea pull-request policy checks for Logisim Revolution."""
import json
import os
import re
import subprocess
import sys
import urllib.error
import urllib.request
from pathlib import Path
CODE_PATHS = (
"src/",
"buildSrc/",
"gradle/",
"support/",
"snap/",
"scripts/",
".gitea/",
)
CODE_FILES = {"build.gradle.kts", "settings.gradle.kts", "gradle.properties", "gradlew", "gradlew.bat"}
DOC_ONLY_PATHS = ("src/main/resources/doc/", "src/main/resources/resources/logisim/strings/")
CLOSING_KEYWORD = re.compile(r"\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*:?[ \t]+", re.I)
def run_git(*args):
return subprocess.check_output(["git", *args], text=True).strip()
def changed_files(base):
return set(run_git("diff", "--name-only", f"{base}...HEAD").splitlines())
def needs_changelog(path):
if path.startswith(DOC_ONLY_PATHS):
return False
return path.startswith(CODE_PATHS) or path in CODE_FILES
def added_changelog_lines(base):
diff = run_git("diff", "--unified=0", f"{base}...HEAD", "--", "CHANGES.md")
line_number = 0
for line in diff.splitlines():
if line.startswith("@@"):
match = re.search(r"\+(\d+)", line)
if match:
line_number = int(match.group(1))
elif line.startswith("+") and not line.startswith("+++"):
if line[1:].strip():
yield line_number
line_number += 1
elif line.startswith(" "):
line_number += 1
def check_changelog(base, author, body):
if "NO_CHANGELOG_ENTRY" in body:
print("Changelog check waived by PR description.")
return
if not any(map(needs_changelog, changed_files(base))):
print("No code or build infrastructure changed; changelog entry not required.")
return
lines = Path("CHANGES.md").read_text(encoding="utf-8").splitlines()
sections = [index for index, line in enumerate(lines, 1) if line.startswith("* ")]
if not sections or not lines[sections[0] - 1].startswith("* @dev"):
raise ValueError("CHANGES.md needs a topmost @dev section.")
end = sections[1] if len(sections) > 1 else len(lines) + 1
entries = [lines[number - 1] for number in added_changelog_lines(base) if sections[0] < number < end]
if not entries:
raise ValueError("Add a line to the topmost @dev section of CHANGES.md, or use NO_CHANGELOG_ENTRY.")
if "NO_CHANGELOG_AUTHOR_CREDIT" in body or os.getenv("NO_CHANGELOG_AUTHOR_CREDIT", "").lower() not in ("", "false", "no", "0"):
print("Changelog entry found; author credit waived.")
return
if not any(re.search(rf"@{re.escape(author)}(?![A-Za-z0-9-])", line, re.I) for line in entries):
raise ValueError(f"New @dev lines must credit @{author}, or use NO_CHANGELOG_AUTHOR_CREDIT.")
print(f"Changelog entry credits @{author}.")
def ticket_numbers(body, server, repo):
prefix = re.escape(f"{server.rstrip('/')}/{repo}/issues/")
refs = re.compile(rf"(?:#|{prefix})(\d+)", re.I)
numbers = set()
for keyword in CLOSING_KEYWORD.finditer(body):
match = refs.match(body, keyword.end())
if match:
numbers.add(int(match.group(1)))
return sorted(numbers)
def api_request(path, *, method="GET", data=None):
token = os.environ["GITEA_TOKEN"]
base = os.environ["GITEA_API_URL"].rstrip("/")
request = urllib.request.Request(
f"{base}/{path.lstrip('/')}",
data=json.dumps(data).encode() if data is not None else None,
headers={"Authorization": f"token {token}", "Content-Type": "application/json"},
method=method,
)
with urllib.request.urlopen(request, timeout=30) as response:
content = response.read()
return json.loads(content) if content else None
def check_tickets(body, author, server, repo):
if "NO_TICKET" in body or author == "dependabot[bot]":
print("Ticket check waived.")
return
numbers = ticket_numbers(body, server, repo)
if not numbers:
raise ValueError("PR description needs an open issue reference such as 'Closes #123', or NO_TICKET.")
for number in numbers:
try:
issue = api_request(f"repos/{repo}/issues/{number}")
except urllib.error.HTTPError as error:
if error.code == 404:
raise ValueError(f"Issue #{number} does not exist in {repo}.") from error
raise
if issue.get("pull_request"):
raise ValueError(f"#{number} is a pull request, not an issue.")
if issue.get("state") != "open":
raise ValueError(f"Issue #{number} is not open.")
print(f"#{number} is open: {issue.get('title', '')}")
def main():
event = json.loads(Path(os.environ["GITEA_EVENT_PATH"]).read_text(encoding="utf-8"))
request = event["pull_request"]
base = request["base"]["sha"]
author = request["user"]["login"]
body = request.get("body") or ""
check_changelog(base, author, body)
check_tickets(body, author, os.environ["GITEA_SERVER_URL"], os.environ["GITEA_REPOSITORY"])
if __name__ == "__main__":
try:
main()
except (ValueError, KeyError, urllib.error.URLError, subprocess.CalledProcessError) as error:
print(f"PR policy failed: {error}", file=sys.stderr)
sys.exit(1)
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""Create and publish a complete manual Gitea release."""
import json
import os
import re
import sys
import urllib.error
import urllib.request
from pathlib import Path
def api(path, method="GET", data=None):
base = os.environ["GITEA_API_URL"].rstrip("/")
request = urllib.request.Request(
f"{base}/repos/{os.environ['GITEA_REPOSITORY']}/{path}",
data=json.dumps(data).encode() if data is not None else None,
headers={
"Authorization": f"token {os.environ['GITEA_TOKEN']}",
"Content-Type": "application/json",
},
method=method,
)
with urllib.request.urlopen(request, timeout=60) as response:
content = response.read()
return json.loads(content) if content else None
def release_tag():
return f"build-{os.environ['GITEA_RUN_ID']}-attempt-{os.environ['GITEA_RUN_ATTEMPT']}"
def prepare():
tag = release_tag()
sha = os.environ["GITEA_SHA"]
body = (
f"Manual build of `{sha}`. Native packages: Linux x86_64 and ARM64, "
"Windows x86_64, macOS Intel and Apple Silicon. The portable JAR needs Java 21+. "
"This release is published only when every target succeeds."
)
try:
release = api(f"releases/tags/{tag}")
except urllib.error.HTTPError as error:
if error.code != 404:
raise
release = api(
"releases",
"POST",
{
"tag_name": tag,
"target_commitish": sha,
"name": f"Logisim Revolution build {sha[:8]}",
"body": body,
"draft": True,
"prerelease": True,
},
)
if not release["draft"]:
raise ValueError(f"Release {tag} is already published.")
if release["target_commitish"] != sha:
raise ValueError(f"Release {tag} points to a different commit.")
release_id = release["id"]
with open(os.environ["GITEA_OUTPUT"], "a", encoding="utf-8") as output:
output.write(f"release_id={release_id}\n")
print(f"Prepared draft release {tag} (ID {release_id}) for {sha}.")
def properties():
content = Path("gradle.properties").read_text(encoding="utf-8")
values = dict(re.findall(r"^\s*(name|version)\s*=\s*(\S+)\s*$", content, re.M))
return values["name"], values["version"].replace("-", ""), values["version"].split("-")[0]
def expected_assets():
name, version, short_version = properties()
return {
f"{name}-{version}-all.jar",
f"{name}-{version}-src.jar",
f"{name}_{version}_amd64.deb",
f"{name}_{version}_arm64.deb",
f"{name}-{version}-1.x86_64.rpm",
f"{name}-{version}-1.aarch64.rpm",
f"{name}-{short_version}-amd64.msi",
f"{name}-{version}-windows-amd64.zip",
f"{name}-{version}-x86_64.dmg",
f"{name}-{version}-aarch64.dmg",
}
def publish():
release_id = os.environ["RELEASE_ID"]
release = api(f"releases/{release_id}")
if not release["draft"]:
raise ValueError("The release is already public.")
if release["tag_name"] != release_tag():
raise ValueError("Release ID does not belong to this workflow run.")
assets = {asset["name"]: asset["size"] for asset in release["assets"]}
missing = sorted(expected_assets() - assets.keys())
empty = sorted(name for name in expected_assets() if name in assets and assets[name] <= 0)
if missing or empty:
raise ValueError(f"Release incomplete; missing: {missing}; empty: {empty}")
api(f"releases/{release_id}", "PATCH", {"draft": False})
print(f"Published {release['html_url']} with {len(expected_assets())} verified attachments.")
if __name__ == "__main__":
try:
if sys.argv[1:] == ["prepare"]:
prepare()
elif sys.argv[1:] == ["publish"]:
publish()
else:
raise ValueError("usage: release.py prepare|publish")
except (ValueError, KeyError, urllib.error.URLError) as error:
print(f"Release failed: {error}", file=sys.stderr)
sys.exit(1)
+84
View File
@@ -0,0 +1,84 @@
"""Checks for release completeness and Gitea PR references."""
import os
import unittest
from unittest.mock import patch
import github_release
import pr_checks
import release
class PullRequestChecksTest(unittest.TestCase):
def test_ticket_references_are_scoped_to_this_gitea_repository(self):
body = (
"Closes #42\nFixes https://git.briggen.dev/NilsBriggen/Logisim-Revolution/issues/7\n"
"Closes https://other.example/other/repo/issues/99"
)
self.assertEqual(
pr_checks.ticket_numbers(
body,
"https://git.briggen.dev",
"NilsBriggen/Logisim-Revolution",
),
[7, 42],
)
def test_documentation_does_not_require_changelog(self):
self.assertFalse(pr_checks.needs_changelog("src/main/resources/doc/en/guide.html"))
self.assertTrue(pr_checks.needs_changelog("src/main/java/com/cburch/logisim/Main.java"))
class GitHubArtifactChecksTest(unittest.TestCase):
def test_artifact_contents_match_each_architecture(self):
self.assertTrue(github_release.valid_names("windows-amd64", ["app.msi", "app.zip"]))
self.assertTrue(github_release.valid_names("macos-amd64", ["app-x86_64.dmg"]))
self.assertTrue(github_release.valid_names("macos-arm64", ["app-aarch64.dmg"]))
self.assertFalse(github_release.valid_names("macos-amd64", ["app-aarch64.dmg"]))
self.assertFalse(github_release.valid_names("windows-amd64", ["app.msi"]))
class ReleaseChecksTest(unittest.TestCase):
def setUp(self):
self.env = patch.dict(
os.environ,
{"RELEASE_ID": "12", "GITEA_RUN_ID": "34", "GITEA_RUN_ATTEMPT": "1"},
)
self.env.start()
self.addCleanup(self.env.stop)
def test_expected_assets_cover_all_five_native_targets(self):
assets = release.expected_assets()
self.assertEqual(len(assets), 10)
name, version, _ = release.properties()
self.assertIn(f"{name}_{version}_arm64.deb", assets)
self.assertIn(f"{name}-{version}-aarch64.dmg", assets)
@patch("release.api")
def test_incomplete_draft_cannot_be_published(self, api):
api.return_value = {
"draft": True,
"tag_name": release.release_tag(),
"assets": [],
}
with self.assertRaisesRegex(ValueError, "Release incomplete"):
release.publish()
api.assert_called_once_with("releases/12")
@patch("release.api")
def test_complete_draft_is_published(self, api):
api.side_effect = [
{
"draft": True,
"tag_name": release.release_tag(),
"assets": [{"name": name, "size": 1} for name in release.expected_assets()],
"html_url": "https://git.example/release",
},
{},
]
release.publish()
api.assert_any_call("releases/12", "PATCH", {"draft": False})
if __name__ == "__main__":
unittest.main()