Rework Gitea CI and cross-platform release builds
This commit is contained in:
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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."
|
||||
@@ -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
|
||||
@@ -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}}"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -296,3 +296,5 @@ gradle-app.setting
|
||||
!/snap/local/usr/bin/
|
||||
*.py
|
||||
!/artwork/generate_revolution_assets.py
|
||||
!/scripts/ci/*.py
|
||||
__pycache__/
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 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
@@ -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.")
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user