mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2025-12-31 01:57:26 +01:00
Merge branch 'main' into copilot/add-wipe-inventory-action
This commit is contained in:
10
.github/ISSUE_TEMPLATE/internal.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/internal.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: "🛠️ Internal / Developer Issue"
|
||||
about: "Unstructured issue for project members only. Outside contributors: please use a standard template."
|
||||
title: "[INT]: "
|
||||
labels: ["internal"]
|
||||
assignees: []
|
||||
---
|
||||
|
||||
**Summary:**
|
||||
[Write here]
|
||||
259
.github/scripts/upgrade-test/README.md
vendored
Normal file
259
.github/scripts/upgrade-test/README.md
vendored
Normal file
@@ -0,0 +1,259 @@
|
||||
# HomeBox Upgrade Testing Workflow
|
||||
|
||||
This document describes the automated upgrade testing workflow for HomeBox.
|
||||
|
||||
## Overview
|
||||
|
||||
The upgrade test workflow is designed to ensure data integrity and functionality when upgrading HomeBox from one version to another. It automatically:
|
||||
|
||||
1. Deploys a stable version of HomeBox
|
||||
2. Creates test data (users, items, locations, labels, notifiers, attachments)
|
||||
3. Upgrades to the latest version from the main branch
|
||||
4. Verifies all data and functionality remain intact
|
||||
|
||||
## Workflow File
|
||||
|
||||
**Location**: `.github/workflows/upgrade-test.yaml`
|
||||
|
||||
## Trigger Conditions
|
||||
|
||||
The workflow runs:
|
||||
- **Daily**: Automatically at 2 AM UTC (via cron schedule)
|
||||
- **Manual**: Can be triggered manually via GitHub Actions UI
|
||||
- **On Push**: When changes are made to the workflow files or test scripts
|
||||
|
||||
## Test Scenarios
|
||||
|
||||
### 1. Environment Setup
|
||||
- Pulls the latest stable HomeBox Docker image from GHCR
|
||||
- Starts the application with test configuration
|
||||
- Ensures the service is healthy and ready
|
||||
|
||||
### 2. Data Creation
|
||||
|
||||
The workflow creates comprehensive test data using the `create-test-data.sh` script:
|
||||
|
||||
#### Users and Groups
|
||||
- **Group 1**: 5 users (user1@homebox.test through user5@homebox.test)
|
||||
- **Group 2**: 2 users (user6@homebox.test and user7@homebox.test)
|
||||
- All users have password: `TestPassword123!`
|
||||
|
||||
#### Locations
|
||||
- **Group 1**: Living Room, Garage
|
||||
- **Group 2**: Home Office
|
||||
|
||||
#### Labels
|
||||
- **Group 1**: Electronics, Important
|
||||
- **Group 2**: Work Equipment
|
||||
|
||||
#### Items
|
||||
- **Group 1**: 5 items (Laptop Computer, Power Drill, TV Remote, Tool Box, Coffee Maker)
|
||||
- **Group 2**: 2 items (Monitor, Keyboard)
|
||||
|
||||
#### Attachments
|
||||
- Multiple attachments added to various items (receipts, manuals, warranties)
|
||||
|
||||
#### Notifiers
|
||||
- **Group 1**: Test notifier named "TESTING"
|
||||
|
||||
### 3. Upgrade Process
|
||||
|
||||
1. Stops the stable version container
|
||||
2. Builds a fresh image from the current main branch
|
||||
3. Copies the database to a new location
|
||||
4. Starts the new version with the existing data
|
||||
|
||||
### 4. Verification Tests
|
||||
|
||||
The Playwright test suite (`upgrade-verification.spec.ts`) verifies:
|
||||
|
||||
- ✅ **User Authentication**: All 7 users can log in with their credentials
|
||||
- ✅ **Data Persistence**: All items, locations, and labels are present
|
||||
- ✅ **Attachments**: File attachments are correctly associated with items
|
||||
- ✅ **Notifiers**: The "TESTING" notifier is still configured
|
||||
- ✅ **UI Functionality**: Version display, theme switching work correctly
|
||||
- ✅ **Data Isolation**: Groups can only see their own data
|
||||
|
||||
## Test Data File
|
||||
|
||||
The setup script generates a JSON file at `/tmp/test-users.json` containing:
|
||||
|
||||
```json
|
||||
{
|
||||
"users": [
|
||||
{
|
||||
"email": "user1@homebox.test",
|
||||
"password": "TestPassword123!",
|
||||
"token": "...",
|
||||
"group": "1"
|
||||
},
|
||||
...
|
||||
],
|
||||
"locations": {
|
||||
"group1": ["location-id-1", "location-id-2"],
|
||||
"group2": ["location-id-3"]
|
||||
},
|
||||
"labels": {...},
|
||||
"items": {...},
|
||||
"notifiers": {...}
|
||||
}
|
||||
```
|
||||
|
||||
This file is used by the Playwright tests to verify data integrity.
|
||||
|
||||
## Scripts
|
||||
|
||||
### create-test-data.sh
|
||||
|
||||
**Location**: `.github/scripts/upgrade-test/create-test-data.sh`
|
||||
|
||||
**Purpose**: Creates all test data via the HomeBox REST API
|
||||
|
||||
**Environment Variables**:
|
||||
- `HOMEBOX_URL`: Base URL of the HomeBox instance (default: http://localhost:7745)
|
||||
- `TEST_DATA_FILE`: Path to output JSON file (default: /tmp/test-users.json)
|
||||
|
||||
**Requirements**:
|
||||
- `curl`: For API calls
|
||||
- `jq`: For JSON processing
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
export HOMEBOX_URL=http://localhost:7745
|
||||
./.github/scripts/upgrade-test/create-test-data.sh
|
||||
```
|
||||
|
||||
## Running Tests Locally
|
||||
|
||||
To run the upgrade tests locally:
|
||||
|
||||
### Prerequisites
|
||||
```bash
|
||||
# Install dependencies
|
||||
sudo apt-get install -y jq curl docker.io
|
||||
|
||||
# Install pnpm and Playwright
|
||||
cd frontend
|
||||
pnpm install
|
||||
pnpm exec playwright install --with-deps chromium
|
||||
```
|
||||
|
||||
### Run the test
|
||||
```bash
|
||||
# Start stable version
|
||||
docker run -d \
|
||||
--name homebox-test \
|
||||
-p 7745:7745 \
|
||||
-e HBOX_OPTIONS_ALLOW_REGISTRATION=true \
|
||||
-v /tmp/homebox-data:/data \
|
||||
ghcr.io/sysadminsmedia/homebox:latest
|
||||
|
||||
# Wait for startup
|
||||
sleep 10
|
||||
|
||||
# Create test data
|
||||
export HOMEBOX_URL=http://localhost:7745
|
||||
./.github/scripts/upgrade-test/create-test-data.sh
|
||||
|
||||
# Stop container
|
||||
docker stop homebox-test
|
||||
docker rm homebox-test
|
||||
|
||||
# Build new version
|
||||
docker build -t homebox:test .
|
||||
|
||||
# Start new version with existing data
|
||||
docker run -d \
|
||||
--name homebox-test \
|
||||
-p 7745:7745 \
|
||||
-e HBOX_OPTIONS_ALLOW_REGISTRATION=true \
|
||||
-v /tmp/homebox-data:/data \
|
||||
homebox:test
|
||||
|
||||
# Wait for startup
|
||||
sleep 10
|
||||
|
||||
# Run verification tests
|
||||
cd frontend
|
||||
TEST_DATA_FILE=/tmp/test-users.json \
|
||||
E2E_BASE_URL=http://localhost:7745 \
|
||||
pnpm exec playwright test \
|
||||
--project=chromium \
|
||||
test/upgrade/upgrade-verification.spec.ts
|
||||
|
||||
# Cleanup
|
||||
docker stop homebox-test
|
||||
docker rm homebox-test
|
||||
```
|
||||
|
||||
## Artifacts
|
||||
|
||||
The workflow produces several artifacts:
|
||||
|
||||
1. **playwright-report-upgrade-test**: HTML report of test results
|
||||
2. **playwright-traces**: Detailed traces for debugging failures
|
||||
3. **Docker logs**: Collected on failure for troubleshooting
|
||||
|
||||
## Failure Scenarios
|
||||
|
||||
The workflow will fail if:
|
||||
- The stable version fails to start
|
||||
- Test data creation fails
|
||||
- The new version fails to start with existing data
|
||||
- Any verification test fails
|
||||
- Database migrations fail
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Test Data Creation Fails
|
||||
|
||||
Check the Docker logs:
|
||||
```bash
|
||||
docker logs homebox-old
|
||||
```
|
||||
|
||||
Verify the API is accessible:
|
||||
```bash
|
||||
curl http://localhost:7745/api/v1/status
|
||||
```
|
||||
|
||||
### Verification Tests Fail
|
||||
|
||||
1. Download the Playwright report from GitHub Actions artifacts
|
||||
2. Review the HTML report for detailed failure information
|
||||
3. Check traces for visual debugging
|
||||
|
||||
### Database Issues
|
||||
|
||||
If migrations fail:
|
||||
```bash
|
||||
# Check database file
|
||||
ls -lh /tmp/homebox-data-new/homebox.db
|
||||
|
||||
# Check Docker logs for migration errors
|
||||
docker logs homebox-new
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements:
|
||||
- [ ] Test multiple upgrade paths (e.g., v0.10 → v0.11 → v0.12)
|
||||
- [ ] Test with PostgreSQL backend in addition to SQLite
|
||||
- [ ] Add performance benchmarks
|
||||
- [ ] Test with larger datasets
|
||||
- [ ] Add API-level verification in addition to UI tests
|
||||
- [ ] Test backup and restore functionality
|
||||
|
||||
## Related Files
|
||||
|
||||
- `.github/workflows/upgrade-test.yaml` - Main workflow definition
|
||||
- `.github/scripts/upgrade-test/create-test-data.sh` - Data generation script
|
||||
- `frontend/test/upgrade/upgrade-verification.spec.ts` - Playwright verification tests
|
||||
- `.github/workflows/e2e-partial.yaml` - Standard E2E test workflow (for reference)
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions about this workflow:
|
||||
1. Check the GitHub Actions run logs
|
||||
2. Review this documentation
|
||||
3. Open an issue in the repository
|
||||
413
.github/scripts/upgrade-test/create-test-data.sh
vendored
Executable file
413
.github/scripts/upgrade-test/create-test-data.sh
vendored
Executable file
@@ -0,0 +1,413 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to create test data in HomeBox for upgrade testing
|
||||
# This script creates users, items, attachments, notifiers, locations, and labels
|
||||
|
||||
set -e
|
||||
|
||||
HOMEBOX_URL="${HOMEBOX_URL:-http://localhost:7745}"
|
||||
API_URL="${HOMEBOX_URL}/api/v1"
|
||||
TEST_DATA_FILE="${TEST_DATA_FILE:-/tmp/test-users.json}"
|
||||
|
||||
echo "Creating test data in HomeBox at $HOMEBOX_URL"
|
||||
|
||||
# Function to make API calls with error handling
|
||||
api_call() {
|
||||
local method=$1
|
||||
local endpoint=$2
|
||||
local data=$3
|
||||
local token=$4
|
||||
|
||||
if [ -n "$token" ]; then
|
||||
if [ -n "$data" ]; then
|
||||
curl -s -X "$method" \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$data" \
|
||||
"$API_URL$endpoint"
|
||||
else
|
||||
curl -s -X "$method" \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$API_URL$endpoint"
|
||||
fi
|
||||
else
|
||||
if [ -n "$data" ]; then
|
||||
curl -s -X "$method" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$data" \
|
||||
"$API_URL$endpoint"
|
||||
else
|
||||
curl -s -X "$method" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$API_URL$endpoint"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to register a user and get token
|
||||
register_user() {
|
||||
local email=$1
|
||||
local name=$2
|
||||
local password=$3
|
||||
local group_token=$4
|
||||
|
||||
echo "Registering user: $email"
|
||||
|
||||
local payload="{\"email\":\"$email\",\"name\":\"$name\",\"password\":\"$password\""
|
||||
|
||||
if [ -n "$group_token" ]; then
|
||||
payload="$payload,\"groupToken\":\"$group_token\""
|
||||
fi
|
||||
|
||||
payload="$payload}"
|
||||
|
||||
local response=$(curl -s -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
"$API_URL/users/register")
|
||||
|
||||
echo "$response"
|
||||
}
|
||||
|
||||
# Function to login and get token
|
||||
login_user() {
|
||||
local email=$1
|
||||
local password=$2
|
||||
|
||||
echo "Logging in user: $email" >&2
|
||||
|
||||
local response=$(curl -s -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"username\":\"$email\",\"password\":\"$password\"}" \
|
||||
"$API_URL/users/login")
|
||||
|
||||
echo "$response" | jq -r '.token // empty'
|
||||
}
|
||||
|
||||
# Function to create an item
|
||||
create_item() {
|
||||
local token=$1
|
||||
local name=$2
|
||||
local description=$3
|
||||
local location_id=$4
|
||||
|
||||
echo "Creating item: $name" >&2
|
||||
|
||||
local payload="{\"name\":\"$name\",\"description\":\"$description\""
|
||||
|
||||
if [ -n "$location_id" ]; then
|
||||
payload="$payload,\"locationId\":\"$location_id\""
|
||||
fi
|
||||
|
||||
payload="$payload}"
|
||||
|
||||
local response=$(curl -s -X POST \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
"$API_URL/items")
|
||||
|
||||
echo "$response"
|
||||
}
|
||||
|
||||
# Function to create a location
|
||||
create_location() {
|
||||
local token=$1
|
||||
local name=$2
|
||||
local description=$3
|
||||
|
||||
echo "Creating location: $name" >&2
|
||||
|
||||
local response=$(curl -s -X POST \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"$name\",\"description\":\"$description\"}" \
|
||||
"$API_URL/locations")
|
||||
|
||||
echo "$response"
|
||||
}
|
||||
|
||||
# Function to create a label
|
||||
create_label() {
|
||||
local token=$1
|
||||
local name=$2
|
||||
local description=$3
|
||||
|
||||
echo "Creating label: $name" >&2
|
||||
|
||||
local response=$(curl -s -X POST \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"$name\",\"description\":\"$description\"}" \
|
||||
"$API_URL/labels")
|
||||
|
||||
echo "$response"
|
||||
}
|
||||
|
||||
# Function to create a notifier
|
||||
create_notifier() {
|
||||
local token=$1
|
||||
local name=$2
|
||||
local url=$3
|
||||
|
||||
echo "Creating notifier: $name" >&2
|
||||
|
||||
local response=$(curl -s -X POST \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"$name\",\"url\":\"$url\",\"isActive\":true}" \
|
||||
"$API_URL/groups/notifiers")
|
||||
|
||||
echo "$response"
|
||||
}
|
||||
|
||||
# Function to attach a file to an item (creates a dummy attachment)
|
||||
attach_file_to_item() {
|
||||
local token=$1
|
||||
local item_id=$2
|
||||
local filename=$3
|
||||
|
||||
echo "Creating attachment for item: $item_id" >&2
|
||||
|
||||
# Create a temporary file with some content
|
||||
local temp_file=$(mktemp)
|
||||
echo "This is a test attachment for $filename" > "$temp_file"
|
||||
|
||||
local response=$(curl -s -X POST \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-F "file=@$temp_file" \
|
||||
-F "type=attachment" \
|
||||
-F "name=$filename" \
|
||||
"$API_URL/items/$item_id/attachments")
|
||||
|
||||
rm -f "$temp_file"
|
||||
|
||||
echo "$response"
|
||||
}
|
||||
|
||||
# Initialize test data storage
|
||||
echo "{\"users\":[]}" > "$TEST_DATA_FILE"
|
||||
|
||||
echo "=== Step 1: Create first group with 5 users ==="
|
||||
|
||||
# Register first user (creates a new group)
|
||||
user1_response=$(register_user "user1@homebox.test" "User One" "TestPassword123!")
|
||||
user1_token=$(echo "$user1_response" | jq -r '.token // empty')
|
||||
group_token=$(echo "$user1_response" | jq -r '.group.inviteToken // empty')
|
||||
|
||||
if [ -z "$user1_token" ]; then
|
||||
echo "Failed to register first user"
|
||||
echo "Response: $user1_response"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "First user registered with token. Group token: $group_token"
|
||||
|
||||
# Store user1 data
|
||||
jq --arg email "user1@homebox.test" \
|
||||
--arg password "TestPassword123!" \
|
||||
--arg token "$user1_token" \
|
||||
--arg group "1" \
|
||||
'.users += [{"email":$email,"password":$password,"token":$token,"group":$group}]' \
|
||||
"$TEST_DATA_FILE" > "$TEST_DATA_FILE.tmp" && mv "$TEST_DATA_FILE.tmp" "$TEST_DATA_FILE"
|
||||
|
||||
# Register 4 more users in the same group
|
||||
for i in {2..5}; do
|
||||
echo "Registering user$i in group 1..."
|
||||
user_response=$(register_user "user${i}@homebox.test" "User $i" "TestPassword123!" "$group_token")
|
||||
user_token=$(echo "$user_response" | jq -r '.token // empty')
|
||||
|
||||
if [ -z "$user_token" ]; then
|
||||
echo "Failed to register user$i"
|
||||
echo "Response: $user_response"
|
||||
else
|
||||
echo "user$i registered successfully"
|
||||
# Store user data
|
||||
jq --arg email "user${i}@homebox.test" \
|
||||
--arg password "TestPassword123!" \
|
||||
--arg token "$user_token" \
|
||||
--arg group "1" \
|
||||
'.users += [{"email":$email,"password":$password,"token":$token,"group":$group}]' \
|
||||
"$TEST_DATA_FILE" > "$TEST_DATA_FILE.tmp" && mv "$TEST_DATA_FILE.tmp" "$TEST_DATA_FILE"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "=== Step 2: Create second group with 2 users ==="
|
||||
|
||||
# Register first user of second group
|
||||
user6_response=$(register_user "user6@homebox.test" "User Six" "TestPassword123!")
|
||||
user6_token=$(echo "$user6_response" | jq -r '.token // empty')
|
||||
group2_token=$(echo "$user6_response" | jq -r '.group.inviteToken // empty')
|
||||
|
||||
if [ -z "$user6_token" ]; then
|
||||
echo "Failed to register user6"
|
||||
echo "Response: $user6_response"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "user6 registered with token. Group 2 token: $group2_token"
|
||||
|
||||
# Store user6 data
|
||||
jq --arg email "user6@homebox.test" \
|
||||
--arg password "TestPassword123!" \
|
||||
--arg token "$user6_token" \
|
||||
--arg group "2" \
|
||||
'.users += [{"email":$email,"password":$password,"token":$token,"group":$group}]' \
|
||||
"$TEST_DATA_FILE" > "$TEST_DATA_FILE.tmp" && mv "$TEST_DATA_FILE.tmp" "$TEST_DATA_FILE"
|
||||
|
||||
# Register second user in group 2
|
||||
user7_response=$(register_user "user7@homebox.test" "User Seven" "TestPassword123!" "$group2_token")
|
||||
user7_token=$(echo "$user7_response" | jq -r '.token // empty')
|
||||
|
||||
if [ -z "$user7_token" ]; then
|
||||
echo "Failed to register user7"
|
||||
echo "Response: $user7_response"
|
||||
else
|
||||
echo "user7 registered successfully"
|
||||
# Store user7 data
|
||||
jq --arg email "user7@homebox.test" \
|
||||
--arg password "TestPassword123!" \
|
||||
--arg token "$user7_token" \
|
||||
--arg group "2" \
|
||||
'.users += [{"email":$email,"password":$password,"token":$token,"group":$group}]' \
|
||||
"$TEST_DATA_FILE" > "$TEST_DATA_FILE.tmp" && mv "$TEST_DATA_FILE.tmp" "$TEST_DATA_FILE"
|
||||
fi
|
||||
|
||||
echo "=== Step 3: Create locations for each group ==="
|
||||
|
||||
# Create locations for group 1 (using user1's token)
|
||||
location1=$(create_location "$user1_token" "Living Room" "Main living area")
|
||||
location1_id=$(echo "$location1" | jq -r '.id // empty')
|
||||
echo "Created location: Living Room (ID: $location1_id)"
|
||||
|
||||
location2=$(create_location "$user1_token" "Garage" "Storage and tools")
|
||||
location2_id=$(echo "$location2" | jq -r '.id // empty')
|
||||
echo "Created location: Garage (ID: $location2_id)"
|
||||
|
||||
# Create location for group 2 (using user6's token)
|
||||
location3=$(create_location "$user6_token" "Home Office" "Work from home space")
|
||||
location3_id=$(echo "$location3" | jq -r '.id // empty')
|
||||
echo "Created location: Home Office (ID: $location3_id)"
|
||||
|
||||
# Store locations
|
||||
jq --arg loc1 "$location1_id" \
|
||||
--arg loc2 "$location2_id" \
|
||||
--arg loc3 "$location3_id" \
|
||||
'.locations = {"group1":[$loc1,$loc2],"group2":[$loc3]}' \
|
||||
"$TEST_DATA_FILE" > "$TEST_DATA_FILE.tmp" && mv "$TEST_DATA_FILE.tmp" "$TEST_DATA_FILE"
|
||||
|
||||
echo "=== Step 4: Create labels for each group ==="
|
||||
|
||||
# Create labels for group 1
|
||||
label1=$(create_label "$user1_token" "Electronics" "Electronic devices")
|
||||
label1_id=$(echo "$label1" | jq -r '.id // empty')
|
||||
echo "Created label: Electronics (ID: $label1_id)"
|
||||
|
||||
label2=$(create_label "$user1_token" "Important" "High priority items")
|
||||
label2_id=$(echo "$label2" | jq -r '.id // empty')
|
||||
echo "Created label: Important (ID: $label2_id)"
|
||||
|
||||
# Create label for group 2
|
||||
label3=$(create_label "$user6_token" "Work Equipment" "Items for work")
|
||||
label3_id=$(echo "$label3" | jq -r '.id // empty')
|
||||
echo "Created label: Work Equipment (ID: $label3_id)"
|
||||
|
||||
# Store labels
|
||||
jq --arg lab1 "$label1_id" \
|
||||
--arg lab2 "$label2_id" \
|
||||
--arg lab3 "$label3_id" \
|
||||
'.labels = {"group1":[$lab1,$lab2],"group2":[$lab3]}' \
|
||||
"$TEST_DATA_FILE" > "$TEST_DATA_FILE.tmp" && mv "$TEST_DATA_FILE.tmp" "$TEST_DATA_FILE"
|
||||
|
||||
echo "=== Step 5: Create test notifier ==="
|
||||
|
||||
# Create notifier for group 1
|
||||
notifier1=$(create_notifier "$user1_token" "TESTING" "https://example.com/webhook")
|
||||
notifier1_id=$(echo "$notifier1" | jq -r '.id // empty')
|
||||
echo "Created notifier: TESTING (ID: $notifier1_id)"
|
||||
|
||||
# Store notifier
|
||||
jq --arg not1 "$notifier1_id" \
|
||||
'.notifiers = {"group1":[$not1]}' \
|
||||
"$TEST_DATA_FILE" > "$TEST_DATA_FILE.tmp" && mv "$TEST_DATA_FILE.tmp" "$TEST_DATA_FILE"
|
||||
|
||||
echo "=== Step 6: Create items for all users ==="
|
||||
|
||||
# Create items for users in group 1
|
||||
declare -A user_tokens
|
||||
user_tokens[1]=$user1_token
|
||||
user_tokens[2]=$(echo "$user1_token") # Users in same group share data, but we'll use user1 token
|
||||
user_tokens[3]=$(echo "$user1_token")
|
||||
user_tokens[4]=$(echo "$user1_token")
|
||||
user_tokens[5]=$(echo "$user1_token")
|
||||
|
||||
# Items for group 1 users
|
||||
echo "Creating items for group 1..."
|
||||
item1=$(create_item "$user1_token" "Laptop Computer" "Dell XPS 15 for work" "$location1_id")
|
||||
item1_id=$(echo "$item1" | jq -r '.id // empty')
|
||||
echo "Created item: Laptop Computer (ID: $item1_id)"
|
||||
|
||||
item2=$(create_item "$user1_token" "Power Drill" "DeWalt 20V cordless drill" "$location2_id")
|
||||
item2_id=$(echo "$item2" | jq -r '.id // empty')
|
||||
echo "Created item: Power Drill (ID: $item2_id)"
|
||||
|
||||
item3=$(create_item "$user1_token" "TV Remote" "Samsung TV remote control" "$location1_id")
|
||||
item3_id=$(echo "$item3" | jq -r '.id // empty')
|
||||
echo "Created item: TV Remote (ID: $item3_id)"
|
||||
|
||||
item4=$(create_item "$user1_token" "Tool Box" "Red metal tool box with tools" "$location2_id")
|
||||
item4_id=$(echo "$item4" | jq -r '.id // empty')
|
||||
echo "Created item: Tool Box (ID: $item4_id)"
|
||||
|
||||
item5=$(create_item "$user1_token" "Coffee Maker" "Breville espresso machine" "$location1_id")
|
||||
item5_id=$(echo "$item5" | jq -r '.id // empty')
|
||||
echo "Created item: Coffee Maker (ID: $item5_id)"
|
||||
|
||||
# Items for group 2 users
|
||||
echo "Creating items for group 2..."
|
||||
item6=$(create_item "$user6_token" "Monitor" "27 inch 4K monitor" "$location3_id")
|
||||
item6_id=$(echo "$item6" | jq -r '.id // empty')
|
||||
echo "Created item: Monitor (ID: $item6_id)"
|
||||
|
||||
item7=$(create_item "$user6_token" "Keyboard" "Mechanical keyboard" "$location3_id")
|
||||
item7_id=$(echo "$item7" | jq -r '.id // empty')
|
||||
echo "Created item: Keyboard (ID: $item7_id)"
|
||||
|
||||
# Store items
|
||||
jq --argjson group1_items "[\"$item1_id\",\"$item2_id\",\"$item3_id\",\"$item4_id\",\"$item5_id\"]" \
|
||||
--argjson group2_items "[\"$item6_id\",\"$item7_id\"]" \
|
||||
'.items = {"group1":$group1_items,"group2":$group2_items}' \
|
||||
"$TEST_DATA_FILE" > "$TEST_DATA_FILE.tmp" && mv "$TEST_DATA_FILE.tmp" "$TEST_DATA_FILE"
|
||||
|
||||
echo "=== Step 7: Add attachments to items ==="
|
||||
|
||||
# Add attachments for group 1 items
|
||||
echo "Adding attachments to group 1 items..."
|
||||
attach_file_to_item "$user1_token" "$item1_id" "laptop-receipt.pdf"
|
||||
attach_file_to_item "$user1_token" "$item1_id" "laptop-warranty.pdf"
|
||||
attach_file_to_item "$user1_token" "$item2_id" "drill-manual.pdf"
|
||||
attach_file_to_item "$user1_token" "$item3_id" "remote-guide.pdf"
|
||||
attach_file_to_item "$user1_token" "$item4_id" "toolbox-inventory.txt"
|
||||
|
||||
# Add attachments for group 2 items
|
||||
echo "Adding attachments to group 2 items..."
|
||||
attach_file_to_item "$user6_token" "$item6_id" "monitor-receipt.pdf"
|
||||
attach_file_to_item "$user6_token" "$item7_id" "keyboard-manual.pdf"
|
||||
|
||||
echo "=== Test Data Creation Complete ==="
|
||||
echo "Test data file saved to: $TEST_DATA_FILE"
|
||||
echo "Summary:"
|
||||
echo " - Users created: 7 (5 in group 1, 2 in group 2)"
|
||||
echo " - Locations created: 3"
|
||||
echo " - Labels created: 3"
|
||||
echo " - Notifiers created: 1"
|
||||
echo " - Items created: 7"
|
||||
echo " - Attachments created: 7"
|
||||
|
||||
# Display the test data file for verification
|
||||
echo ""
|
||||
echo "Test data:"
|
||||
cat "$TEST_DATA_FILE" | jq '.'
|
||||
|
||||
exit 0
|
||||
16
.github/workflows/binaries-publish.yaml
vendored
16
.github/workflows/binaries-publish.yaml
vendored
@@ -17,19 +17,17 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c
|
||||
with:
|
||||
go-version: "1.24"
|
||||
cache-dependency-path: backend/go.mod
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 9.15.3
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
|
||||
|
||||
- name: Build Frontend and Copy to Backend
|
||||
working-directory: frontend
|
||||
@@ -51,7 +49,7 @@ jobs:
|
||||
- name: Run GoReleaser
|
||||
id: releaser
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a
|
||||
with:
|
||||
workdir: "backend"
|
||||
distribution: goreleaser
|
||||
@@ -75,7 +73,7 @@ jobs:
|
||||
|
||||
- name: Run GoReleaser No Release
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a
|
||||
with:
|
||||
workdir: "backend"
|
||||
distribution: goreleaser
|
||||
@@ -93,7 +91,7 @@ jobs:
|
||||
actions: read # To read the workflow path.
|
||||
id-token: write # To sign the provenance.
|
||||
contents: write # To add assets to a release.
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.9.0
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@f7dd8c54c2067bafc12ca7a55595d5ee9b75204a
|
||||
with:
|
||||
base64-subjects: "${{ needs.goreleaser.outputs.hashes }}"
|
||||
upload-assets: true # upload to a new release
|
||||
@@ -105,7 +103,7 @@ jobs:
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Install the verifier
|
||||
uses: slsa-framework/slsa-verifier/actions/installer@v2.4.0
|
||||
uses: slsa-framework/slsa-verifier/actions/installer@ea584f4502babc6f60d9bc799dbbb13c1caa9ee6
|
||||
|
||||
- name: Download assets
|
||||
env:
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
permissions:
|
||||
packages: write
|
||||
steps:
|
||||
- uses: dataaxiom/ghcr-cleanup-action@v1
|
||||
- uses: dataaxiom/ghcr-cleanup-action@cd0cdb900b5dbf3a6f2cc869f0dbb0b8211f50c4
|
||||
with:
|
||||
dry-run: true
|
||||
delete-ghost-images: true
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
permissions:
|
||||
packages: write
|
||||
steps:
|
||||
- uses: dataaxiom/ghcr-cleanup-action@v1
|
||||
- uses: dataaxiom/ghcr-cleanup-action@cd0cdb900b5dbf3a6f2cc869f0dbb0b8211f50c4
|
||||
with:
|
||||
dry-run: false
|
||||
delete-untagged: true
|
||||
|
||||
14
.github/workflows/copilot-setup-steps.yml
vendored
14
.github/workflows/copilot-setup-steps.yml
vendored
@@ -26,25 +26,23 @@ jobs:
|
||||
# If you do not check out your code, Copilot will do this for you.
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "24"
|
||||
|
||||
- uses: pnpm/action-setup@v3.0.0
|
||||
with:
|
||||
version: 9.12.2
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c
|
||||
with:
|
||||
go-version: "1.24"
|
||||
cache-dependency-path: backend/go.mod
|
||||
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v1
|
||||
uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
19
.github/workflows/docker-publish-hardened.yaml
vendored
19
.github/workflows/docker-publish-hardened.yaml
vendored
@@ -33,7 +33,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -43,10 +43,11 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
- linux/arm/v7
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
- name: Enable Debug Logs
|
||||
@@ -56,7 +57,7 @@ jobs:
|
||||
ACTIONS_STEP_DEBUG: true
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
@@ -123,7 +124,7 @@ jobs:
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
|
||||
- name: Attest platform-specific images
|
||||
uses: actions/attest-build-provenance@v1
|
||||
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
subject-name: ${{ env.GHCR_REPO }}
|
||||
@@ -216,7 +217,7 @@ jobs:
|
||||
echo "digest=$digest" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Attest GHCR images
|
||||
uses: actions/attest-build-provenance@v1
|
||||
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
subject-name: ${{ env.GHCR_REPO }}
|
||||
@@ -240,7 +241,7 @@ jobs:
|
||||
echo "digest=$digest" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Attest Dockerhub images
|
||||
uses: actions/attest-build-provenance@v1
|
||||
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
with:
|
||||
subject-name: docker.io/${{ env.DOCKERHUB_REPO }}
|
||||
|
||||
43
.github/workflows/docker-publish-rootless.yaml
vendored
43
.github/workflows/docker-publish-rootless.yaml
vendored
@@ -37,7 +37,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -47,10 +47,11 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
- linux/arm/v7
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
- name: Enable Debug Logs
|
||||
@@ -60,7 +61,7 @@ jobs:
|
||||
ACTIONS_STEP_DEBUG: true
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
@@ -75,40 +76,40 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
|
||||
with:
|
||||
images: |
|
||||
name=${{ env.DOCKERHUB_REPO }},enable=${{ github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') }}
|
||||
name=${{ env.GHCR_REPO }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392
|
||||
with:
|
||||
image: ghcr.io/sysadminsmedia/binfmt:latest
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
|
||||
with:
|
||||
driver-opts: |
|
||||
image=ghcr.io/sysadminsmedia/buildkit:master
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
|
||||
with:
|
||||
context: . # Explicitly specify the build context
|
||||
file: ./Dockerfile.rootless # Explicitly specify the Dockerfile
|
||||
@@ -125,7 +126,7 @@ jobs:
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
|
||||
- name: Attest platform-specific images
|
||||
uses: actions/attest-build-provenance@v1
|
||||
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
subject-name: ${{ env.GHCR_REPO }}
|
||||
@@ -139,7 +140,7 @@ jobs:
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
@@ -159,35 +160,35 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
|
||||
with:
|
||||
driver-opts: |
|
||||
image=ghcr.io/sysadminsmedia/buildkit:master
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
|
||||
with:
|
||||
images: |
|
||||
name=${{ env.DOCKERHUB_REPO }},enable=${{ github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') }}
|
||||
@@ -218,7 +219,7 @@ jobs:
|
||||
echo "digest=$digest" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Attest GHCR images
|
||||
uses: actions/attest-build-provenance@v1
|
||||
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
subject-name: ${{ env.GHCR_REPO }}
|
||||
@@ -242,7 +243,7 @@ jobs:
|
||||
echo "digest=$digest" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Attest Dockerhub images
|
||||
uses: actions/attest-build-provenance@v1
|
||||
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
with:
|
||||
subject-name: docker.io/${{ env.DOCKERHUB_REPO }}
|
||||
|
||||
43
.github/workflows/docker-publish.yaml
vendored
43
.github/workflows/docker-publish.yaml
vendored
@@ -37,7 +37,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read # Allows access to repository contents (read-only)
|
||||
packages: write # Allows pushing to GHCR
|
||||
@@ -47,14 +47,15 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
- linux/arm/v7
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
@@ -70,40 +71,40 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
|
||||
with:
|
||||
images: |
|
||||
name=${{ env.DOCKERHUB_REPO }},enable=${{ github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') }}
|
||||
name=${{ env.GHCR_REPO }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392
|
||||
with:
|
||||
image: ghcr.io/sysadminsmedia/binfmt:latest
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
|
||||
with:
|
||||
driver-opts: |
|
||||
image=ghcr.io/sysadminsmedia/buildkit:latest
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
|
||||
with:
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -118,7 +119,7 @@ jobs:
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
|
||||
- name: Attest platform-specific images
|
||||
uses: actions/attest-build-provenance@v1
|
||||
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
subject-name: ${{ env.GHCR_REPO }}
|
||||
@@ -132,7 +133,7 @@ jobs:
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
@@ -152,35 +153,35 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
|
||||
with:
|
||||
driver-opts: |
|
||||
image=ghcr.io/sysadminsmedia/buildkit:master
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
|
||||
with:
|
||||
images: |
|
||||
name=${{ env.DOCKERHUB_REPO }},enable=${{ github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') }}
|
||||
@@ -209,7 +210,7 @@ jobs:
|
||||
echo "digest=$digest" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Attest GHCR images
|
||||
uses: actions/attest-build-provenance@v1
|
||||
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
subject-name: ${{ env.GHCR_REPO }}
|
||||
@@ -233,7 +234,7 @@ jobs:
|
||||
echo "digest=$digest" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Attest Dockerhub images
|
||||
uses: actions/attest-build-provenance@v1
|
||||
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
with:
|
||||
subject-name: docker.io/${{ env.DOCKERHUB_REPO }}
|
||||
|
||||
34
.github/workflows/e2e-partial.yaml
vendored
34
.github/workflows/e2e-partial.yaml
vendored
@@ -1,5 +1,11 @@
|
||||
name: E2E (Playwright)
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
checks: write
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
@@ -15,28 +21,26 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v1
|
||||
uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c
|
||||
with:
|
||||
go-version: "1.23"
|
||||
go-version: "1.24"
|
||||
cache-dependency-path: backend/go.mod
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- uses: pnpm/action-setup@v3.0.0
|
||||
with:
|
||||
version: 9.12.2
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
@@ -49,7 +53,7 @@ jobs:
|
||||
- name: Run E2E Tests
|
||||
run: task test:e2e -- --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
name: Upload partial Playwright report
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
@@ -64,20 +68,18 @@ jobs:
|
||||
name: Merge Playwright Reports
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
|
||||
with:
|
||||
node-version: lts/*
|
||||
- uses: pnpm/action-setup@v3.0.0
|
||||
with:
|
||||
version: 9.12.2
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
working-directory: frontend
|
||||
|
||||
- name: Download blob reports from GitHub Actions Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
path: frontend/all-blob-reports
|
||||
pattern: blob-report-*
|
||||
@@ -88,7 +90,7 @@ jobs:
|
||||
working-directory: frontend
|
||||
|
||||
- name: Upload HTML report
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: html-report--attempt-${{ github.run_attempt }}
|
||||
path: frontend/playwright-report
|
||||
|
||||
50
.github/workflows/issue-gatekeeper.yml
vendored
Normal file
50
.github/workflows/issue-gatekeeper.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
name: Issue Gatekeeper
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [ opened ]
|
||||
|
||||
jobs:
|
||||
check-permissions:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Verify Internal Template Use
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const issue_number = context.issue.number;
|
||||
const actor = context.payload.sender.login;
|
||||
|
||||
// 1. Get user permission level
|
||||
const { data: perms } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner,
|
||||
repo,
|
||||
username: actor
|
||||
});
|
||||
|
||||
const isMember = ['admin', 'write'].includes(perms.permission);
|
||||
const body = context.payload.issue.body || "";
|
||||
|
||||
// 2. Check if they used the internal template (or if the issue is blank)
|
||||
// We detect this by checking for our specific template string or the 'internal' label
|
||||
const usedInternal = context.payload.issue.labels.some(l => l.name === 'internal');
|
||||
|
||||
if (usedInternal && !isMember) {
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
body: `@${actor}, the "Internal" template is restricted to project members. Please use one of the standard bug or feature templates for this repository.`
|
||||
});
|
||||
|
||||
await github.rest.issues.update({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
state: 'closed'
|
||||
});
|
||||
}
|
||||
14
.github/workflows/partial-backend.yaml
vendored
14
.github/workflows/partial-backend.yaml
vendored
@@ -1,5 +1,11 @@
|
||||
name: Go Build/Test
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
checks: write
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
@@ -7,21 +13,21 @@ jobs:
|
||||
Go:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c
|
||||
with:
|
||||
go-version: "1.24"
|
||||
cache-dependency-path: backend/go.mod
|
||||
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v1
|
||||
uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v7
|
||||
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20
|
||||
with:
|
||||
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
|
||||
version: latest
|
||||
|
||||
42
.github/workflows/partial-frontend.yaml
vendored
42
.github/workflows/partial-frontend.yaml
vendored
@@ -1,5 +1,11 @@
|
||||
name: Frontend
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
checks: write
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
@@ -9,13 +15,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: pnpm/action-setup@v3.0.0
|
||||
with:
|
||||
version: 9.12.2
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
@@ -48,28 +52,26 @@ jobs:
|
||||
--health-retries 5
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v1
|
||||
uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c
|
||||
with:
|
||||
go-version: "1.23"
|
||||
go-version: "1.24"
|
||||
cache-dependency-path: backend/go.mod
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: lts/*
|
||||
|
||||
- uses: pnpm/action-setup@v3.0.0
|
||||
with:
|
||||
version: 9.12.2
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
@@ -99,28 +101,26 @@ jobs:
|
||||
- 5432:5432
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v1
|
||||
uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c
|
||||
with:
|
||||
go-version: "1.23"
|
||||
go-version: "1.24"
|
||||
cache-dependency-path: backend/go.mod
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- uses: pnpm/action-setup@v3.0.0
|
||||
with:
|
||||
version: 9.12.2
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
6
.github/workflows/pull-requests.yaml
vendored
6
.github/workflows/pull-requests.yaml
vendored
@@ -1,5 +1,11 @@
|
||||
name: Pull Request CI
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
checks: write
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
|
||||
6
.github/workflows/update-currencies.yml
vendored
6
.github/workflows/update-currencies.yml
vendored
@@ -15,12 +15,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548
|
||||
with:
|
||||
python-version: '3.8'
|
||||
cache: 'pip'
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
if: env.changed == 'true'
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: update-currencies
|
||||
|
||||
177
.github/workflows/upgrade-test.yaml
vendored
Normal file
177
.github/workflows/upgrade-test.yaml
vendored
Normal file
@@ -0,0 +1,177 @@
|
||||
#name: HomeBox Upgrade Test
|
||||
|
||||
# on:
|
||||
# schedule:
|
||||
# Run daily at 2 AM UTC
|
||||
# - cron: '0 2 * * *'
|
||||
# workflow_dispatch: # Allow manual trigger
|
||||
# push:
|
||||
# branches:
|
||||
# - main
|
||||
# paths:
|
||||
# - '.github/workflows/upgrade-test.yaml'
|
||||
# - '.github/scripts/upgrade-test/**'
|
||||
|
||||
jobs:
|
||||
upgrade-test:
|
||||
name: Test Upgrade Path
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read # Read repository contents
|
||||
packages: read # Pull Docker images from GHCR
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3.0.0
|
||||
with:
|
||||
version: 9.12.2
|
||||
|
||||
- name: Install Playwright
|
||||
run: |
|
||||
cd frontend
|
||||
pnpm install
|
||||
pnpm exec playwright install --with-deps chromium
|
||||
|
||||
- name: Create test data directory
|
||||
run: |
|
||||
mkdir -p /tmp/homebox-data-old
|
||||
mkdir -p /tmp/homebox-data-new
|
||||
chmod -R 777 /tmp/homebox-data-old
|
||||
chmod -R 777 /tmp/homebox-data-new
|
||||
|
||||
# Step 1: Pull and deploy latest stable version
|
||||
- name: Pull latest stable HomeBox image
|
||||
run: |
|
||||
docker pull ghcr.io/sysadminsmedia/homebox:latest
|
||||
|
||||
- name: Start HomeBox (stable version)
|
||||
run: |
|
||||
docker run -d \
|
||||
--name homebox-old \
|
||||
--restart unless-stopped \
|
||||
-p 7745:7745 \
|
||||
-e HBOX_LOG_LEVEL=debug \
|
||||
-e HBOX_OPTIONS_ALLOW_REGISTRATION=true \
|
||||
-e TZ=UTC \
|
||||
-v /tmp/homebox-data-old:/data \
|
||||
ghcr.io/sysadminsmedia/homebox:latest
|
||||
|
||||
# Wait for the service to be ready
|
||||
timeout 60 bash -c 'until curl -f http://localhost:7745/api/v1/status; do sleep 2; done'
|
||||
echo "HomeBox stable version is ready"
|
||||
|
||||
# Step 2: Create test data
|
||||
- name: Create test data
|
||||
run: |
|
||||
chmod +x .github/scripts/upgrade-test/create-test-data.sh
|
||||
.github/scripts/upgrade-test/create-test-data.sh
|
||||
env:
|
||||
HOMEBOX_URL: http://localhost:7745
|
||||
|
||||
- name: Verify initial data creation
|
||||
run: |
|
||||
echo "Verifying test data was created..."
|
||||
# Check if database file exists and has content
|
||||
if [ -f /tmp/homebox-data-old/homebox.db ]; then
|
||||
ls -lh /tmp/homebox-data-old/homebox.db
|
||||
echo "Database file exists"
|
||||
else
|
||||
echo "Database file not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Stop old HomeBox instance
|
||||
run: |
|
||||
docker stop homebox-old
|
||||
docker rm homebox-old
|
||||
|
||||
# Step 3: Build latest version from main branch
|
||||
- name: Build HomeBox from main branch
|
||||
run: |
|
||||
docker build \
|
||||
--build-arg VERSION=main \
|
||||
--build-arg COMMIT=${{ github.sha }} \
|
||||
--build-arg BUILD_TIME="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
|
||||
-t homebox:test \
|
||||
-f Dockerfile \
|
||||
.
|
||||
|
||||
# Step 4: Copy data and start new version
|
||||
- name: Copy data to new location
|
||||
run: |
|
||||
cp -r /tmp/homebox-data-old/* /tmp/homebox-data-new/
|
||||
chmod -R 777 /tmp/homebox-data-new
|
||||
|
||||
- name: Start HomeBox (new version)
|
||||
run: |
|
||||
docker run -d \
|
||||
--name homebox-new \
|
||||
--restart unless-stopped \
|
||||
-p 7745:7745 \
|
||||
-e HBOX_LOG_LEVEL=debug \
|
||||
-e HBOX_OPTIONS_ALLOW_REGISTRATION=true \
|
||||
-e TZ=UTC \
|
||||
-v /tmp/homebox-data-new:/data \
|
||||
homebox:test
|
||||
|
||||
# Wait for the service to be ready
|
||||
timeout 60 bash -c 'until curl -f http://localhost:7745/api/v1/status; do sleep 2; done'
|
||||
echo "HomeBox new version is ready"
|
||||
|
||||
# Step 5: Run verification tests with Playwright
|
||||
- name: Run verification tests
|
||||
run: |
|
||||
cd frontend
|
||||
TEST_DATA_FILE=/tmp/test-users.json \
|
||||
E2E_BASE_URL=http://localhost:7745 \
|
||||
pnpm exec playwright test \
|
||||
-c ./test/playwright.config.ts \
|
||||
--project=chromium \
|
||||
test/upgrade/upgrade-verification.spec.ts
|
||||
env:
|
||||
HOMEBOX_URL: http://localhost:7745
|
||||
|
||||
- name: Upload Playwright report
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report-upgrade-test
|
||||
path: frontend/playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload test traces
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-traces
|
||||
path: frontend/test-results/
|
||||
retention-days: 7
|
||||
|
||||
- name: Collect logs on failure
|
||||
if: failure()
|
||||
run: |
|
||||
echo "=== Docker logs for new version ==="
|
||||
docker logs homebox-new || true
|
||||
echo "=== Database content ==="
|
||||
ls -la /tmp/homebox-data-new/ || true
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: |
|
||||
docker stop homebox-new || true
|
||||
docker rm homebox-new || true
|
||||
docker rmi homebox:test || true
|
||||
@@ -17,8 +17,6 @@ builds:
|
||||
- freebsd
|
||||
goarch:
|
||||
- amd64
|
||||
- "386"
|
||||
- arm
|
||||
- arm64
|
||||
- riscv64
|
||||
flags:
|
||||
@@ -28,20 +26,9 @@ builds:
|
||||
- -X main.version={{.Version}}
|
||||
- -X main.commit={{.Commit}}
|
||||
- -X main.date={{.Date}}
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
- goos: windows
|
||||
goarch: "386"
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
- goos: freebsd
|
||||
goarch: "386"
|
||||
tags:
|
||||
- >-
|
||||
{{- if eq .Arch "riscv64" }}nodynamic
|
||||
{{- else if eq .Arch "arm" }}nodynamic
|
||||
{{- else if eq .Arch "386" }}nodynamic
|
||||
{{- else if eq .Os "freebsd" }}nodynamic
|
||||
{{ end }}
|
||||
|
||||
@@ -62,7 +49,6 @@ archives:
|
||||
{{ .ProjectName }}_
|
||||
{{- title .Os }}_
|
||||
{{- if eq .Arch "amd64" }}x86_64
|
||||
{{- else if eq .Arch "386" }}i386
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
# use zip for windows archives
|
||||
|
||||
@@ -108,7 +108,7 @@ func run(cfg *config.Config) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.ToLower(cfg.Database.Driver) == "postgres" {
|
||||
if strings.ToLower(cfg.Database.Driver) == config.DriverPostgres {
|
||||
if !validatePostgresSSLMode(cfg.Database.SslMode) {
|
||||
log.Error().Str("sslmode", cfg.Database.SslMode).Msg("invalid sslmode")
|
||||
return fmt.Errorf("invalid sslmode: %s", cfg.Database.SslMode)
|
||||
|
||||
@@ -41,7 +41,7 @@ func setupStorageDir(cfg *config.Config) error {
|
||||
func setupDatabaseURL(cfg *config.Config) (string, error) {
|
||||
databaseURL := ""
|
||||
switch strings.ToLower(cfg.Database.Driver) {
|
||||
case "sqlite3":
|
||||
case config.DriverSqlite3:
|
||||
databaseURL = cfg.Database.SqlitePath
|
||||
dbFilePath := strings.Split(cfg.Database.SqlitePath, "?")[0]
|
||||
dbDir := filepath.Dir(dbFilePath)
|
||||
@@ -49,7 +49,7 @@ func setupDatabaseURL(cfg *config.Config) (string, error) {
|
||||
log.Error().Err(err).Str("path", dbDir).Msg("failed to create SQLite database directory")
|
||||
return "", fmt.Errorf("failed to create SQLite database directory: %w", err)
|
||||
}
|
||||
case "postgres":
|
||||
case config.DriverPostgres:
|
||||
databaseURL = fmt.Sprintf("host=%s port=%s dbname=%s sslmode=%s", cfg.Database.Host, cfg.Database.Port, cfg.Database.Database, cfg.Database.SslMode)
|
||||
if cfg.Database.Username != "" {
|
||||
databaseURL += fmt.Sprintf(" user=%s", cfg.Database.Username)
|
||||
|
||||
5
backend/internal/data/ent/item_predicates.go
generated
5
backend/internal/data/ent/item_predicates.go
generated
@@ -4,6 +4,7 @@ import (
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/item"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/predicate"
|
||||
conf "github.com/sysadminsmedia/homebox/backend/internal/sys/config"
|
||||
"github.com/sysadminsmedia/homebox/backend/pkgs/textutils"
|
||||
)
|
||||
|
||||
@@ -24,7 +25,7 @@ func AccentInsensitiveContains(field string, searchValue string) predicate.Item
|
||||
dialect := s.Dialect()
|
||||
|
||||
switch dialect {
|
||||
case "sqlite3":
|
||||
case conf.DriverSqlite3:
|
||||
// For SQLite, we'll create a custom normalization function using REPLACE
|
||||
// to handle common accented characters
|
||||
normalizeFunc := buildSQLiteNormalizeExpression(s.C(field))
|
||||
@@ -32,7 +33,7 @@ func AccentInsensitiveContains(field string, searchValue string) predicate.Item
|
||||
"LOWER("+normalizeFunc+") LIKE ?",
|
||||
"%"+normalizedSearch+"%",
|
||||
))
|
||||
case "postgres":
|
||||
case conf.DriverPostgres:
|
||||
// For PostgreSQL, use REPLACE-based normalization to avoid unaccent dependency
|
||||
normalizeFunc := buildGenericNormalizeExpression(s.C(field))
|
||||
// Use sql.P() for proper PostgreSQL parameter binding ($1, $2, etc.)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
|
||||
)
|
||||
|
||||
//go:embed all:postgres
|
||||
@@ -21,9 +22,9 @@ var sqliteFiles embed.FS
|
||||
// embedded file system containing the migration files for the specified dialect.
|
||||
func Migrations(dialect string) (embed.FS, error) {
|
||||
switch dialect {
|
||||
case "postgres":
|
||||
case config.DriverPostgres:
|
||||
return postgresFiles, nil
|
||||
case "sqlite3":
|
||||
case config.DriverSqlite3:
|
||||
return sqliteFiles, nil
|
||||
default:
|
||||
log.Error().Str("dialect", dialect).Msg("unknown sql dialect")
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package config
|
||||
|
||||
const (
|
||||
DriverSqlite3 = "sqlite3"
|
||||
DriverSqlite3 = "sqlite3"
|
||||
DriverPostgres = "postgres"
|
||||
)
|
||||
|
||||
type Storage struct {
|
||||
|
||||
@@ -43,6 +43,7 @@ export default defineConfig({
|
||||
nav: [
|
||||
{ text: 'API Docs', link: '/en/api' },
|
||||
{ text: 'Demo', link: 'https://demo.homebox.software' },
|
||||
{ text: 'Blog', link: 'https://sysadminsjournal.com/tag/homebox/' }
|
||||
],
|
||||
|
||||
sidebar: {
|
||||
|
||||
418
frontend/test/upgrade/upgrade-verification.spec.ts
Normal file
418
frontend/test/upgrade/upgrade-verification.spec.ts
Normal file
@@ -0,0 +1,418 @@
|
||||
/**
|
||||
* HomeBox Upgrade Verification Tests
|
||||
*
|
||||
* NOTE: These tests are ONLY meant to run in the upgrade-test workflow.
|
||||
* They require test data to be pre-created by the create-test-data.sh script.
|
||||
* These tests are stored in test/upgrade/ (not test/e2e/) to prevent them
|
||||
* from running during normal E2E test runs.
|
||||
*/
|
||||
|
||||
import { expect, test } from "@playwright/test";
|
||||
import * as fs from "fs";
|
||||
|
||||
// Load test data created by the setup script
|
||||
const testDataPath = process.env.TEST_DATA_FILE || "/tmp/test-users.json";
|
||||
|
||||
interface TestUser {
|
||||
email: string;
|
||||
password: string;
|
||||
token: string;
|
||||
group: string;
|
||||
}
|
||||
|
||||
interface TestData {
|
||||
users?: TestUser[];
|
||||
locations?: Record<string, string[]>;
|
||||
labels?: Record<string, string[]>;
|
||||
items?: Record<string, string[]>;
|
||||
notifiers?: Record<string, string[]>;
|
||||
}
|
||||
|
||||
let testData: TestData = {};
|
||||
|
||||
test.beforeAll(() => {
|
||||
if (fs.existsSync(testDataPath)) {
|
||||
const rawData = fs.readFileSync(testDataPath, "utf-8");
|
||||
testData = JSON.parse(rawData);
|
||||
console.log("Loaded test data:", JSON.stringify(testData, null, 2));
|
||||
} else {
|
||||
console.error(`Test data file not found at ${testDataPath}`);
|
||||
throw new Error("Test data file not found");
|
||||
}
|
||||
});
|
||||
|
||||
test.describe("HomeBox Upgrade Verification", () => {
|
||||
test("verify all users can log in", async ({ page }) => {
|
||||
// Test each user from the test data
|
||||
for (const user of testData.users || []) {
|
||||
await page.goto("/");
|
||||
await expect(page).toHaveURL("/");
|
||||
|
||||
// Wait for login form to be ready
|
||||
await page.waitForSelector("input[type='text']", { state: "visible" });
|
||||
|
||||
// Fill in login form
|
||||
await page.fill("input[type='text']", user.email);
|
||||
await page.fill("input[type='password']", user.password);
|
||||
await page.click("button[type='submit']");
|
||||
|
||||
// Wait for navigation to home page
|
||||
await expect(page).toHaveURL("/home", { timeout: 10000 });
|
||||
|
||||
console.log(`✓ User ${user.email} logged in successfully`);
|
||||
|
||||
// Navigate back to login for next user
|
||||
await page.goto("/");
|
||||
await page.waitForSelector("input[type='text']", { state: "visible" });
|
||||
}
|
||||
});
|
||||
|
||||
test("verify application version is displayed", async ({ page }) => {
|
||||
// Login as first user
|
||||
const firstUser = testData.users?.[0];
|
||||
if (!firstUser) {
|
||||
throw new Error("No users found in test data");
|
||||
}
|
||||
|
||||
await page.goto("/");
|
||||
await page.fill("input[type='text']", firstUser.email);
|
||||
await page.fill("input[type='password']", firstUser.password);
|
||||
await page.click("button[type='submit']");
|
||||
await expect(page).toHaveURL("/home", { timeout: 10000 });
|
||||
|
||||
// Look for version in footer or about section
|
||||
// The version might be in the footer or a settings page
|
||||
// Check if footer exists and contains version info
|
||||
const footer = page.locator("footer");
|
||||
if ((await footer.count()) > 0) {
|
||||
const footerText = await footer.textContent();
|
||||
console.log("Footer text:", footerText);
|
||||
|
||||
// Version should be present in some form
|
||||
// This is a basic check - the version format may vary
|
||||
expect(footerText).toBeTruthy();
|
||||
}
|
||||
|
||||
console.log("✓ Application version check complete");
|
||||
});
|
||||
|
||||
test("verify locations are present", async ({ page }) => {
|
||||
const firstUser = testData.users?.[0];
|
||||
if (!firstUser) {
|
||||
throw new Error("No users found in test data");
|
||||
}
|
||||
|
||||
await page.goto("/");
|
||||
await page.fill("input[type='text']", firstUser.email);
|
||||
await page.fill("input[type='password']", firstUser.password);
|
||||
await page.click("button[type='submit']");
|
||||
await expect(page).toHaveURL("/home", { timeout: 10000 });
|
||||
|
||||
// Wait for page to load
|
||||
await page.waitForSelector("body", { state: "visible" });
|
||||
|
||||
// Try to find locations link in navigation
|
||||
const locationsLink = page.locator("a[href*='location'], button:has-text('Locations')").first();
|
||||
|
||||
if ((await locationsLink.count()) > 0) {
|
||||
await locationsLink.click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Check if locations are displayed
|
||||
// The exact structure depends on the UI, but we should see location names
|
||||
const pageContent = await page.textContent("body");
|
||||
|
||||
// Verify some of our test locations exist
|
||||
expect(pageContent).toContain("Living Room");
|
||||
console.log("✓ Locations verified");
|
||||
} else {
|
||||
console.log("! Could not find locations navigation - skipping detailed check");
|
||||
}
|
||||
});
|
||||
|
||||
test("verify labels are present", async ({ page }) => {
|
||||
const firstUser = testData.users?.[0];
|
||||
if (!firstUser) {
|
||||
throw new Error("No users found in test data");
|
||||
}
|
||||
|
||||
await page.goto("/");
|
||||
await page.fill("input[type='text']", firstUser.email);
|
||||
await page.fill("input[type='password']", firstUser.password);
|
||||
await page.click("button[type='submit']");
|
||||
await expect(page).toHaveURL("/home", { timeout: 10000 });
|
||||
|
||||
await page.waitForSelector("body", { state: "visible" });
|
||||
|
||||
// Try to find labels link in navigation
|
||||
const labelsLink = page.locator("a[href*='label'], button:has-text('Labels')").first();
|
||||
|
||||
if ((await labelsLink.count()) > 0) {
|
||||
await labelsLink.click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
const pageContent = await page.textContent("body");
|
||||
|
||||
// Verify some of our test labels exist
|
||||
expect(pageContent).toContain("Electronics");
|
||||
console.log("✓ Labels verified");
|
||||
} else {
|
||||
console.log("! Could not find labels navigation - skipping detailed check");
|
||||
}
|
||||
});
|
||||
|
||||
test("verify items are present", async ({ page }) => {
|
||||
const firstUser = testData.users?.[0];
|
||||
if (!firstUser) {
|
||||
throw new Error("No users found in test data");
|
||||
}
|
||||
|
||||
await page.goto("/");
|
||||
await page.fill("input[type='text']", firstUser.email);
|
||||
await page.fill("input[type='password']", firstUser.password);
|
||||
await page.click("button[type='submit']");
|
||||
await expect(page).toHaveURL("/home", { timeout: 10000 });
|
||||
|
||||
await page.waitForSelector("body", { state: "visible" });
|
||||
|
||||
// Navigate to items list
|
||||
// This might be the home page or a separate items page
|
||||
const itemsLink = page.locator("a[href*='item'], button:has-text('Items')").first();
|
||||
|
||||
if ((await itemsLink.count()) > 0) {
|
||||
await itemsLink.click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
}
|
||||
|
||||
const pageContent = await page.textContent("body");
|
||||
|
||||
// Verify some of our test items exist
|
||||
expect(pageContent).toContain("Laptop Computer");
|
||||
console.log("✓ Items verified");
|
||||
});
|
||||
|
||||
test("verify notifier is present", async ({ page }) => {
|
||||
const firstUser = testData.users?.[0];
|
||||
if (!firstUser) {
|
||||
throw new Error("No users found in test data");
|
||||
}
|
||||
|
||||
await page.goto("/");
|
||||
await page.fill("input[type='text']", firstUser.email);
|
||||
await page.fill("input[type='password']", firstUser.password);
|
||||
await page.click("button[type='submit']");
|
||||
await expect(page).toHaveURL("/home", { timeout: 10000 });
|
||||
|
||||
await page.waitForSelector("body", { state: "visible" });
|
||||
|
||||
// Navigate to settings or profile
|
||||
// Notifiers are typically in settings
|
||||
const settingsLink = page.locator("a[href*='setting'], a[href*='profile'], button:has-text('Settings')").first();
|
||||
|
||||
if ((await settingsLink.count()) > 0) {
|
||||
await settingsLink.click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Look for notifiers section
|
||||
const notifiersLink = page.locator("a:has-text('Notif'), button:has-text('Notif')").first();
|
||||
|
||||
if ((await notifiersLink.count()) > 0) {
|
||||
await notifiersLink.click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
const pageContent = await page.textContent("body");
|
||||
|
||||
// Verify our test notifier exists
|
||||
expect(pageContent).toContain("TESTING");
|
||||
console.log("✓ Notifier verified");
|
||||
} else {
|
||||
console.log("! Could not find notifiers section - skipping detailed check");
|
||||
}
|
||||
} else {
|
||||
console.log("! Could not find settings navigation - skipping notifier check");
|
||||
}
|
||||
});
|
||||
|
||||
test("verify attachments are present for items", async ({ page }) => {
|
||||
const firstUser = testData.users?.[0];
|
||||
if (!firstUser) {
|
||||
throw new Error("No users found in test data");
|
||||
}
|
||||
|
||||
await page.goto("/");
|
||||
await page.fill("input[type='text']", firstUser.email);
|
||||
await page.fill("input[type='password']", firstUser.password);
|
||||
await page.click("button[type='submit']");
|
||||
await expect(page).toHaveURL("/home", { timeout: 10000 });
|
||||
|
||||
await page.waitForSelector("body", { state: "visible" });
|
||||
|
||||
// Search for "Laptop Computer" which should have attachments
|
||||
const searchInput = page.locator("input[type='search'], input[placeholder*='Search']").first();
|
||||
|
||||
if ((await searchInput.count()) > 0) {
|
||||
await searchInput.fill("Laptop Computer");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Click on the laptop item
|
||||
const laptopItem = page.locator("text=Laptop Computer").first();
|
||||
await laptopItem.click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Look for attachments section
|
||||
const pageContent = await page.textContent("body");
|
||||
|
||||
// Check for attachment indicators (could be files, documents, attachments, etc.)
|
||||
const hasAttachments =
|
||||
pageContent?.includes("laptop-receipt") ||
|
||||
pageContent?.includes("laptop-warranty") ||
|
||||
pageContent?.includes("attachment") ||
|
||||
pageContent?.includes("Attachment") ||
|
||||
pageContent?.includes("document");
|
||||
|
||||
expect(hasAttachments).toBeTruthy();
|
||||
console.log("✓ Attachments verified");
|
||||
} else {
|
||||
console.log("! Could not find search - trying direct navigation");
|
||||
|
||||
// Try alternative: look for items link and browse
|
||||
const itemsLink = page.locator("a[href*='item'], button:has-text('Items')").first();
|
||||
if ((await itemsLink.count()) > 0) {
|
||||
await itemsLink.click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
const laptopLink = page.locator("text=Laptop Computer").first();
|
||||
if ((await laptopLink.count()) > 0) {
|
||||
await laptopLink.click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
const pageContent = await page.textContent("body");
|
||||
const hasAttachments =
|
||||
pageContent?.includes("laptop-receipt") ||
|
||||
pageContent?.includes("laptop-warranty") ||
|
||||
pageContent?.includes("attachment");
|
||||
|
||||
expect(hasAttachments).toBeTruthy();
|
||||
console.log("✓ Attachments verified via direct navigation");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("verify theme can be adjusted", async ({ page }) => {
|
||||
const firstUser = testData.users?.[0];
|
||||
if (!firstUser) {
|
||||
throw new Error("No users found in test data");
|
||||
}
|
||||
|
||||
await page.goto("/");
|
||||
await page.fill("input[type='text']", firstUser.email);
|
||||
await page.fill("input[type='password']", firstUser.password);
|
||||
await page.click("button[type='submit']");
|
||||
await expect(page).toHaveURL("/home", { timeout: 10000 });
|
||||
|
||||
await page.waitForSelector("body", { state: "visible" });
|
||||
|
||||
// Look for theme toggle (usually a sun/moon icon or settings)
|
||||
// Common selectors for theme toggles
|
||||
const themeToggle = page
|
||||
.locator(
|
||||
"button[aria-label*='theme'], button[aria-label*='Theme'], " +
|
||||
"button:has-text('Dark'), button:has-text('Light'), " +
|
||||
"[data-theme-toggle], .theme-toggle"
|
||||
)
|
||||
.first();
|
||||
|
||||
if ((await themeToggle.count()) > 0) {
|
||||
// Get initial theme state (could be from class, attribute, or computed style)
|
||||
const bodyBefore = page.locator("body");
|
||||
const classNameBefore = (await bodyBefore.getAttribute("class")) || "";
|
||||
|
||||
// Click theme toggle
|
||||
await themeToggle.click();
|
||||
// Wait for theme change to complete
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Get theme state after toggle
|
||||
const classNameAfter = (await bodyBefore.getAttribute("class")) || "";
|
||||
|
||||
// Verify that something changed
|
||||
expect(classNameBefore).not.toBe(classNameAfter);
|
||||
|
||||
console.log(`✓ Theme toggle working (${classNameBefore} -> ${classNameAfter})`);
|
||||
} else {
|
||||
// Try to find theme in settings
|
||||
const settingsLink = page.locator("a[href*='setting'], a[href*='profile']").first();
|
||||
|
||||
if ((await settingsLink.count()) > 0) {
|
||||
await settingsLink.click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
const themeOption = page.locator("select[name*='theme'], button:has-text('Theme')").first();
|
||||
|
||||
if ((await themeOption.count()) > 0) {
|
||||
console.log("✓ Theme settings found");
|
||||
} else {
|
||||
console.log("! Could not find theme toggle - feature may not be easily accessible");
|
||||
}
|
||||
} else {
|
||||
console.log("! Could not find theme controls");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("verify data counts match expectations", async ({ page }) => {
|
||||
const firstUser = testData.users?.[0];
|
||||
if (!firstUser) {
|
||||
throw new Error("No users found in test data");
|
||||
}
|
||||
|
||||
await page.goto("/");
|
||||
await page.fill("input[type='text']", firstUser.email);
|
||||
await page.fill("input[type='password']", firstUser.password);
|
||||
await page.click("button[type='submit']");
|
||||
await expect(page).toHaveURL("/home", { timeout: 10000 });
|
||||
|
||||
await page.waitForSelector("body", { state: "visible" });
|
||||
|
||||
// Check that we have the expected number of items for group 1 (5 items)
|
||||
const pageContent = await page.textContent("body");
|
||||
|
||||
// Look for item count indicators
|
||||
// This is dependent on the UI showing counts
|
||||
console.log("✓ Logged in and able to view dashboard");
|
||||
|
||||
// Verify at least that the page loaded and shows some content
|
||||
expect(pageContent).toBeTruthy();
|
||||
if (pageContent) {
|
||||
expect(pageContent.length).toBeGreaterThan(100);
|
||||
}
|
||||
});
|
||||
|
||||
test("verify second group users and data isolation", async ({ page }) => {
|
||||
// Login as user from group 2
|
||||
const group2User = testData.users?.find(u => u.group === "2");
|
||||
if (!group2User) {
|
||||
console.log("! No group 2 users found - skipping isolation test");
|
||||
return;
|
||||
}
|
||||
|
||||
await page.goto("/");
|
||||
await page.fill("input[type='text']", group2User.email);
|
||||
await page.fill("input[type='password']", group2User.password);
|
||||
await page.click("button[type='submit']");
|
||||
await expect(page).toHaveURL("/home", { timeout: 10000 });
|
||||
|
||||
await page.waitForSelector("body", { state: "visible" });
|
||||
|
||||
const pageContent = await page.textContent("body");
|
||||
|
||||
// Verify group 2 can see their items
|
||||
expect(pageContent).toContain("Monitor");
|
||||
|
||||
// Verify group 2 cannot see group 1 items
|
||||
expect(pageContent).not.toContain("Laptop Computer");
|
||||
|
||||
console.log("✓ Data isolation verified between groups");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user