mirror of
https://codeberg.org/secana/Forji.git
synced 2026-06-16 05:13:55 -07:00
Closes #3. Adds an "Actions" tab to the repository view, surfacing Forgejo Actions runs and (experimentally) their jobs, steps, and logs. ## What's in this branch 1. ~~**`chore: point ForgejoKit at local checkout for Actions PR`** — temporary `XCLocalSwiftPackageReference` so the feature commit compiles against the unreleased ForgejoKit changes. Drop this commit before merge and replace with `chore: bump ForgejoKit to <new version>` once the ForgejoKit PR ships in a release.~~ **`chore: update ForgejoKit to released version 0.4.0`** + **`chore: add ForgejoKit 0.4.0 to Package.resolved`** — switches from local path override to a remote pin at `secana/ForgejoKit` `0.4.0`. 2. **`feat: add Actions tab to repository view`** — the actual feature. ## UI scope - New `Actions` tab, shown only when `repository.hasActions == true`. - Runs list with All / Running / Success / Failed filter, pagination, pull-to-refresh, empty state. - Run detail with header, metadata (workflow file, event, trigger user, started/duration — zero-Date suppressed), and an experimental Jobs section. - Job view with collapsible step rows; logs lazy-load on expand via `logCursors`. - "Open in browser" toolbar item using each run's `html_url`. ## Experimental jobs/steps view Forgejo's `/api/v1` doesn't expose jobs, steps, or step logs for a run. This PR opts into ForgejoKit's experimental `fetchRunView` (backed by Forgejo's web-UI route) so we can render a GitHub-Actions-style view today. The Jobs section and Steps screen are explicitly labelled "Experimental" in the UI, and a footer notes the output may change between Forgejo versions. Happy to drop this section if you'd rather ship public-API only first. ## Defensive handling - Forgejo serialises an unset `time.Time` as `0001-01-01T00:00:00Z` (showed up as "Started 56 yrs, 4 mths" / "Duration 493 906h" on a cancelled run). Sanitised display helpers suppress any pre-2000 date. - The experimental `fetchRunView` resolves Forgejo's `RedirectToLatestAttempt` server-side before POSTing, so attempts with non-zero attempt numbers work. (Without this, Forgejo returns 500 "task with job_id N and attempt 0: resource does not exist".) ## Tests - Unit: `WorkflowRunFilterTests`, `WorkflowStatusIconTests` (filter mapping, status enum compatibility against Forgejo's documented values, status icon mapping, run helpers, zero-date guard). - UI: `ActionsUITests` (read-only smoke test). Co-authored-by: Voislav Vasiljevski <voislav@voioo.cz> Reviewed-on: https://codeberg.org/secana/Forji/pulls/28
328 lines
13 KiB
Makefile
328 lines
13 KiB
Makefile
set shell := ["bash", "-eo", "pipefail", "-c"]
|
|
|
|
default_destination := "platform=iOS Simulator,name=iPhone 17 Pro"
|
|
default_destination_b := "platform=iOS Simulator,name=iPhone Air"
|
|
default_destination_c := "platform=iOS Simulator,name=iPhone 17 Pro Max"
|
|
compose_file := "integration/docker-compose.yml"
|
|
base_url_1 := "http://localhost:13001"
|
|
base_url_2 := "http://localhost:13002"
|
|
base_url_3 := "http://localhost:13003"
|
|
readonly_classes := "LoginUITests CommitHistoryUITests PaginationUITests RepositoryUITests IssueUITests PullRequestUITests OverviewCreateUITests HomeScreenUITests PermissionUITests MergedInstanceReadOnlyUITests ActionsUITests"
|
|
mutating_a_classes := "MergedInstanceUITests IssueMutatingUITests NotificationsUITests OverviewCreateMutatingUITests"
|
|
mutating_b_classes := "PullRequestMutatingUITests RepositoryMutatingUITests"
|
|
|
|
# List all available tasks
|
|
default:
|
|
@just --list
|
|
|
|
# Build the app
|
|
build destination=default_destination:
|
|
xcodebuild -project Forji/Forji.xcodeproj -scheme Forji -destination '{{destination}}' build 2>&1 | xcbeautify
|
|
|
|
# Run app unit tests
|
|
test destination=default_destination:
|
|
xcodebuild -project Forji/Forji.xcodeproj -scheme Forji -destination '{{destination}}' test -only-testing:ForjiTests 2>&1 | xcbeautify
|
|
|
|
# Lint Swift code
|
|
lint:
|
|
swiftlint lint Forji/Forji
|
|
|
|
# Format Swift code
|
|
format:
|
|
swiftformat Forji/Forji
|
|
|
|
pbxproj := "Forji/Forji.xcodeproj/project.pbxproj"
|
|
|
|
# Show current app version
|
|
version:
|
|
@grep -m1 'MARKETING_VERSION' {{pbxproj}} | sed 's/.*= *//;s/;.*//'
|
|
|
|
# Set app version (updates both MARKETING_VERSION and CURRENT_PROJECT_VERSION)
|
|
set-version new_version:
|
|
sed -i '' 's/MARKETING_VERSION = [^;]*/MARKETING_VERSION = {{new_version}}/' {{pbxproj}}
|
|
sed -i '' 's/CURRENT_PROJECT_VERSION = [^;]*/CURRENT_PROJECT_VERSION = {{new_version}}/' {{pbxproj}}
|
|
@echo "Version set to {{new_version}}"
|
|
|
|
# Clean build artifacts
|
|
clean:
|
|
xcodebuild -project Forji/Forji.xcodeproj -scheme Forji clean 2>&1 | xcbeautify
|
|
|
|
# Build, install, and launch in simulator
|
|
run destination=default_destination:
|
|
#!/usr/bin/env bash
|
|
set -eo pipefail
|
|
SIM_NAME="$(echo '{{destination}}' | sed -n 's/.*name=\([^,]*\).*/\1/p')"
|
|
xcrun simctl boot "$SIM_NAME" 2>/dev/null || true
|
|
open -a Simulator
|
|
xcodebuild -project Forji/Forji.xcodeproj -scheme Forji -destination '{{destination}}' build 2>&1 | xcbeautify
|
|
BUILT_APP="$(xcodebuild -project Forji/Forji.xcodeproj -scheme Forji -destination '{{destination}}' -showBuildSettings 2>/dev/null | grep ' BUILT_PRODUCTS_DIR' | awk '{print $3}')/Forji.app"
|
|
xcrun simctl install "$SIM_NAME" "$BUILT_APP"
|
|
xcrun simctl launch "$SIM_NAME" "$(defaults read "$BUILT_APP/Info.plist" CFBundleIdentifier)"
|
|
|
|
# List all UI integration tests
|
|
test-list:
|
|
@grep -rh 'func test.*()' Forji/ForjiUITests/*.swift \
|
|
| grep -v 'override\|private\|ForgejoUITestBase' \
|
|
| sed 's/.*func //' | sed 's/().*//' \
|
|
| while read -r method; do \
|
|
file=$(grep -rl "func $method()" Forji/ForjiUITests/*.swift | head -1); \
|
|
class=$(grep 'class.*:' "$file" | head -1 | sed 's/.*class //' | sed 's/[: ].*//' ); \
|
|
printf " %s/%s\n" "$class" "$method"; \
|
|
done | sort
|
|
|
|
# Start Forgejo Docker containers
|
|
docker-up:
|
|
docker compose -f {{compose_file}} up -d --wait --wait-timeout 120
|
|
|
|
# Stop Forgejo Docker containers and clean up
|
|
docker-down:
|
|
docker compose -f {{compose_file}} down -v 2>/dev/null || true
|
|
rm -f /tmp/forgejo_test_url.txt /tmp/forgejo_test_url_*.txt
|
|
rm -f /tmp/forgejo_test_url2.txt /tmp/forgejo_test_url2_*.txt
|
|
rm -f /tmp/forgejo_test_token.txt /tmp/forgejo_test_token_*.txt
|
|
|
|
# Clear integration test caches (seed snapshots, temp files)
|
|
clean-integration:
|
|
rm -f integration/.forgejo-seed-snapshot.tar.gz integration/.forgejo-seed-hash
|
|
rm -f /tmp/forgejo_test_url.txt /tmp/forgejo_test_url_*.txt
|
|
rm -f /tmp/forgejo_test_url2.txt /tmp/forgejo_test_url2_*.txt
|
|
rm -f /tmp/forgejo_test_token.txt /tmp/forgejo_test_token_*.txt
|
|
rm -rf integration/forgejo-seed/.build
|
|
@echo "Integration test caches cleared."
|
|
|
|
# Seed test data into Forgejo instances (with snapshot caching)
|
|
seed:
|
|
#!/usr/bin/env bash
|
|
set -eo pipefail
|
|
COMPOSE_FILE="{{compose_file}}"
|
|
BASE_URLS=("{{base_url_1}}" "{{base_url_2}}" "{{base_url_3}}")
|
|
SERVICES=(forgejo-1 forgejo-2 forgejo-3)
|
|
SNAPSHOT_FILE="integration/.forgejo-seed-snapshot.tar.gz"
|
|
HASH_FILE="integration/.forgejo-seed-hash"
|
|
SETUP_HASH=$(find integration/forgejo-seed/Sources -name '*.swift' -print0 | sort -z | xargs -0 shasum -a 256 | shasum -a 256 | awk '{print $1}')
|
|
wait_for_instance() {
|
|
local url="$1" elapsed=0
|
|
until curl -sf "$url/api/v1/version" > /dev/null 2>&1; do
|
|
if [ "$elapsed" -ge 60 ]; then
|
|
echo "Timed out waiting for $url after 60s"
|
|
exit 1
|
|
fi
|
|
sleep 1
|
|
elapsed=$((elapsed + 1))
|
|
done
|
|
echo " $url is ready (${elapsed}s)"
|
|
}
|
|
if [ -f "$SNAPSHOT_FILE" ] && [ -f "$HASH_FILE" ] && [ "$(cat "$HASH_FILE")" = "$SETUP_HASH" ]; then
|
|
echo "Restoring from seed snapshot..."
|
|
for svc in "${SERVICES[@]}"; do
|
|
CID=$(docker compose -f "$COMPOSE_FILE" ps -q "$svc")
|
|
docker cp "$SNAPSHOT_FILE" "$CID:/tmp/snapshot.tar.gz" &
|
|
done
|
|
wait
|
|
for svc in "${SERVICES[@]}"; do
|
|
CID=$(docker compose -f "$COMPOSE_FILE" ps -q "$svc")
|
|
docker exec "$CID" sh -c "cd / && tar xzf /tmp/snapshot.tar.gz" &
|
|
done
|
|
wait
|
|
docker compose -f "$COMPOSE_FILE" restart "${SERVICES[@]}"
|
|
for url in "${BASE_URLS[@]}"; do
|
|
wait_for_instance "$url" &
|
|
done
|
|
wait
|
|
echo "Snapshot restored to all instances."
|
|
else
|
|
echo "Seeding test data..."
|
|
swift run --package-path integration/forgejo-seed forgejo-seed "${BASE_URLS[0]}" forgejo-1
|
|
echo "Snapshotting seed data..."
|
|
CID_1=$(docker compose -f "$COMPOSE_FILE" ps -q forgejo-1)
|
|
docker exec "$CID_1" sh -c "cd / && tar czf /tmp/snapshot.tar.gz data"
|
|
docker cp "$CID_1:/tmp/snapshot.tar.gz" "$SNAPSHOT_FILE"
|
|
echo "$SETUP_HASH" > "$HASH_FILE"
|
|
echo "Restoring snapshot to other instances..."
|
|
for svc in "${SERVICES[@]:1}"; do
|
|
CID=$(docker compose -f "$COMPOSE_FILE" ps -q "$svc")
|
|
docker cp "$SNAPSHOT_FILE" "$CID:/tmp/snapshot.tar.gz"
|
|
docker exec "$CID" sh -c "cd / && tar xzf /tmp/snapshot.tar.gz" &
|
|
done
|
|
wait
|
|
docker compose -f "$COMPOSE_FILE" restart "${SERVICES[@]:1}"
|
|
for url in "${BASE_URLS[@]:1}"; do
|
|
wait_for_instance "$url" &
|
|
done
|
|
wait
|
|
echo "All instances seeded."
|
|
fi
|
|
|
|
# Run read-only UI tests (assumes build + seed + URL files are set up)
|
|
test-readonly destination=default_destination:
|
|
#!/usr/bin/env bash
|
|
set -eo pipefail
|
|
ARGS=""
|
|
for cls in {{readonly_classes}}; do
|
|
ARGS="$ARGS -only-testing:ForjiUITests/$cls"
|
|
done
|
|
xcodebuild test-without-building \
|
|
-project Forji/Forji.xcodeproj \
|
|
-scheme Forji \
|
|
-destination '{{destination}}' \
|
|
-parallel-testing-enabled YES \
|
|
$ARGS 2>&1 | xcbeautify
|
|
|
|
# Run mutating group A: MergedInstanceUITests (needs 2 instances)
|
|
test-mutating-a destination=default_destination_b:
|
|
#!/usr/bin/env bash
|
|
set -eo pipefail
|
|
ARGS=""
|
|
for cls in {{mutating_a_classes}}; do
|
|
ARGS="$ARGS -only-testing:ForjiUITests/$cls"
|
|
done
|
|
xcodebuild test-without-building \
|
|
-project Forji/Forji.xcodeproj \
|
|
-scheme Forji \
|
|
-destination '{{destination}}' \
|
|
-parallel-testing-enabled NO \
|
|
$ARGS 2>&1 | xcbeautify
|
|
|
|
# Run mutating group B: all non-merged mutating tests
|
|
test-mutating-b destination=default_destination_c:
|
|
#!/usr/bin/env bash
|
|
set -eo pipefail
|
|
ARGS=""
|
|
for cls in {{mutating_b_classes}}; do
|
|
ARGS="$ARGS -only-testing:ForjiUITests/$cls"
|
|
done
|
|
xcodebuild test-without-building \
|
|
-project Forji/Forji.xcodeproj \
|
|
-scheme Forji \
|
|
-destination '{{destination}}' \
|
|
-parallel-testing-enabled NO \
|
|
$ARGS 2>&1 | xcbeautify
|
|
|
|
# Run full UI integration test suite (requires Docker)
|
|
test-ui destination_a=default_destination destination_b=default_destination_b destination_c=default_destination_c:
|
|
#!/usr/bin/env bash
|
|
set -eo pipefail
|
|
cleanup() {
|
|
just docker-down
|
|
}
|
|
trap cleanup EXIT
|
|
just sim-boot '{{destination_a}}' '{{destination_b}}' '{{destination_c}}' &
|
|
just docker-up &
|
|
just build-for-testing '{{destination_a}}' '{{destination_b}}' '{{destination_c}}' &
|
|
wait
|
|
just seed
|
|
just _write-url-files '{{destination_a}}' '{{destination_b}}' '{{destination_c}}'
|
|
LOG_RO="/tmp/forji_test_readonly.log"
|
|
LOG_MA="/tmp/forji_test_mutating_a.log"
|
|
LOG_MB="/tmp/forji_test_mutating_b.log"
|
|
EXIT_RO=0
|
|
EXIT_MA=0
|
|
EXIT_MB=0
|
|
echo "Starting read-only tests on {{destination_a}}..."
|
|
just test-readonly '{{destination_a}}' > "$LOG_RO" 2>&1 &
|
|
PID_RO=$!
|
|
echo "Starting mutating-a (merged) tests on {{destination_b}}..."
|
|
just test-mutating-a '{{destination_b}}' > "$LOG_MA" 2>&1 &
|
|
PID_MA=$!
|
|
echo "Starting mutating-b (rest) tests on {{destination_c}}..."
|
|
just test-mutating-b '{{destination_c}}' > "$LOG_MB" 2>&1 &
|
|
PID_MB=$!
|
|
wait $PID_RO || EXIT_RO=$?
|
|
wait $PID_MA || EXIT_MA=$?
|
|
wait $PID_MB || EXIT_MB=$?
|
|
echo "=== Read-only tests output ==="
|
|
cat "$LOG_RO"
|
|
echo ""
|
|
echo "=== Mutating-A (merged) tests output ==="
|
|
cat "$LOG_MA"
|
|
echo ""
|
|
echo "=== Mutating-B (rest) tests output ==="
|
|
cat "$LOG_MB"
|
|
rm -f "$LOG_RO" "$LOG_MA" "$LOG_MB"
|
|
FAILED=0
|
|
if [ "$EXIT_RO" -ne 0 ]; then echo "Read-only tests failed (exit $EXIT_RO)."; FAILED=1; fi
|
|
if [ "$EXIT_MA" -ne 0 ]; then echo "Mutating-A tests failed (exit $EXIT_MA)."; FAILED=1; fi
|
|
if [ "$EXIT_MB" -ne 0 ]; then echo "Mutating-B tests failed (exit $EXIT_MB)."; FAILED=1; fi
|
|
if [ "$FAILED" -ne 0 ]; then
|
|
echo "Integration tests FAILED."
|
|
exit 1
|
|
fi
|
|
echo ""
|
|
echo "All integration tests passed."
|
|
|
|
# Run a single UI test (requires Docker). Use `just test-list` to see available tests.
|
|
test-one filter="" destination=default_destination:
|
|
#!/usr/bin/env bash
|
|
set -eo pipefail
|
|
FILTER="{{filter}}"
|
|
if [ -z "$FILTER" ]; then
|
|
echo "Usage: just test-one <Class/method>"
|
|
echo ""
|
|
echo "Run 'just test-list' to see available tests."
|
|
exit 1
|
|
fi
|
|
cleanup() {
|
|
just docker-down
|
|
}
|
|
trap cleanup EXIT
|
|
just sim-boot '{{destination}}' '{{destination}}' '{{destination}}' &
|
|
just docker-up &
|
|
just build-for-testing '{{destination}}' '{{destination}}' '{{destination}}' &
|
|
wait
|
|
just seed
|
|
just _write-url-file '{{destination}}' '{{base_url_1}}' true
|
|
just _write-url2-file '{{destination}}' '{{base_url_2}}' true
|
|
xcodebuild test-without-building \
|
|
-project Forji/Forji.xcodeproj \
|
|
-scheme Forji \
|
|
-destination '{{destination}}' \
|
|
-parallel-testing-enabled NO \
|
|
-only-testing:"ForjiUITests/$FILTER" 2>&1 | xcbeautify
|
|
|
|
[private]
|
|
sim-boot destination_a=default_destination destination_b=default_destination_b destination_c=default_destination_c:
|
|
#!/usr/bin/env bash
|
|
set -eo pipefail
|
|
for dest in '{{destination_a}}' '{{destination_b}}' '{{destination_c}}'; do
|
|
SIM="$(echo "$dest" | sed -n 's/.*name=\([^,]*\).*/\1/p')"
|
|
xcrun simctl boot "$SIM" 2>/dev/null || true
|
|
done
|
|
|
|
[private]
|
|
build-for-testing destination_a=default_destination destination_b=default_destination_b destination_c=default_destination_c:
|
|
xcodebuild build-for-testing \
|
|
-project Forji/Forji.xcodeproj \
|
|
-scheme Forji \
|
|
-destination '{{destination_a}}' \
|
|
-destination '{{destination_b}}' \
|
|
-destination '{{destination_c}}' 2>&1 | xcbeautify
|
|
|
|
[private]
|
|
_write-url-file destination url fallback="false":
|
|
#!/usr/bin/env bash
|
|
set -eo pipefail
|
|
SIM="$(echo '{{destination}}' | sed -n 's/.*name=\([^,]*\).*/\1/p')"
|
|
SIM_SAFE="${SIM// /_}"
|
|
echo "{{url}}" > "/tmp/forgejo_test_url_${SIM_SAFE}.txt"
|
|
if [ "{{fallback}}" = "true" ]; then
|
|
echo "{{url}}" > /tmp/forgejo_test_url.txt
|
|
fi
|
|
|
|
[private]
|
|
_write-url2-file destination url fallback="false":
|
|
#!/usr/bin/env bash
|
|
set -eo pipefail
|
|
SIM="$(echo '{{destination}}' | sed -n 's/.*name=\([^,]*\).*/\1/p')"
|
|
SIM_SAFE="${SIM// /_}"
|
|
echo "{{url}}" > "/tmp/forgejo_test_url2_${SIM_SAFE}.txt"
|
|
if [ "{{fallback}}" = "true" ]; then
|
|
echo "{{url}}" > /tmp/forgejo_test_url2.txt
|
|
fi
|
|
|
|
[private]
|
|
_write-url-files destination_a=default_destination destination_b=default_destination_b destination_c=default_destination_c:
|
|
just _write-url-file '{{destination_a}}' '{{base_url_1}}' true
|
|
just _write-url-file '{{destination_b}}' '{{base_url_2}}'
|
|
just _write-url-file '{{destination_c}}' '{{base_url_3}}'
|
|
just _write-url2-file '{{destination_a}}' '{{base_url_2}}' true
|
|
just _write-url2-file '{{destination_b}}' '{{base_url_1}}'
|