Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 36 additions & 136 deletions .github/workflows/build_release.yml
Original file line number Diff line number Diff line change
@@ -1,21 +1,3 @@
# This workflow is used to create a (pre-)release build of the OpenList frontend.
#
# This will:
#
# - Update the `package.json` version to the specified version (when triggered
# by `workflow_dispatch`), commit the changes and tag it.
# - Upload the release assets to GitHub.
# - Publish the package to npm.
#
# # Usage
#
# This workflow can be triggered by:
#
# - Pushing a tag that matches the pattern `v[0-9]+.[0-9]+.[0-9]+*` (semver format).
# - Manually via the GitHub Actions UI with a version input.
#
# To create (pre-)release builds, we recommend that you use the `workflow_dispatch`.

name: Release Build

on:
Expand All @@ -25,9 +7,9 @@ on:
workflow_dispatch:
inputs:
version:
description: |
Target version (e.g., 1.0.0), will create a tag named 'v<version>' and update package.json version
required: true
description: "Target version (e.g., 1.0.0)"
required: false
default: "1.0.0"
type: string

jobs:
Expand All @@ -37,100 +19,41 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive

- name: Validate and trim semver
id: semver
uses: matt-usurp/validate-semver@v2
with:
version: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version || github.ref_name }}

- name: Check if version is pre-release
id: check_pre_release
run: |
if [[ -z "${{ steps.semver.outputs.prerelease }}" && -z "${{ steps.semver.outputs.build }}" ]]; then
echo "is_pre_release=false" >> $GITHUB_OUTPUT
else
echo "is_pre_release=true" >> $GITHUB_OUTPUT
fi

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: "24.14.1"
registry-url: "https://registry.npmjs.org"

- name: Install pnpm
uses: pnpm/action-setup@v5
with:
run_install: false

- name: Import GPG key
if: github.event_name == 'workflow_dispatch'
uses: crazy-max/ghaction-import-gpg@v7
with:
gpg_private_key: ${{ secrets.BOT_GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.BOT_GPG_PASSPHRASE }}
git_user_signingkey: true
git_commit_gpgsign: true
git_tag_gpgsign: true

- name: Setup CI Bot
if: github.event_name == 'workflow_dispatch'
- name: Configure Git identity
run: |
git config user.name "${{ secrets.BOT_USERNAME }}"
git config user.email "${{ secrets.BOT_USEREMAIL }}"

- name: Update package.json and commit
if: github.event_name == 'workflow_dispatch'
run: |
jq --arg version "${{ steps.semver.outputs.version }}" '.version = $version' package.json > package.json.tmp && mv package.json.tmp package.json
git add package.json
git commit -S -m "chore: release v${{ steps.semver.outputs.version }}" --no-verify
git push

# For local build, needn't push, the tag will be created by `gh release create`
git tag -s "v${{ steps.semver.outputs.version }}" -m "Release v${{ steps.semver.outputs.version }}"
git config user.name "GitHub Actions"
git config user.email "actions@github.com"

- name: Get current tag
id: get_current_tag
- name: Determine release tag
id: tag
run: |
# Remove existing tag `rolling`, since `rolling` should always point to the latest commit
# This is necessary to avoid conflicts with the changelog generation
git tag -d rolling 2>/dev/null || true

# Get the current tag
CURRENT_TAG=$(git describe --tags --abbrev=0)
echo "current_tag=$CURRENT_TAG" >> $GITHUB_OUTPUT

# Temporarily remove all pre-release tags (tags containing '-' / '+')
# This prevents them from interfering with changelog generation
PRE_RELEASE_TAGS=$(git tag -l | grep -E "(-|\+)" || true)
if [ -n "$PRE_RELEASE_TAGS" ]; then
echo "Temporarily removing pre-release tags: $PRE_RELEASE_TAGS"
echo "$PRE_RELEASE_TAGS" | xargs -r git tag -d
fi

# Add back the current tag if is a pre-release
# Should not add `-f`, as it will overwrite the existing tag
if [[ "${{ steps.check_pre_release.outputs.is_pre_release }}" == "true" ]]; then
git tag -s "$CURRENT_TAG" -m "Release $CURRENT_TAG"
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
# 使用时间戳作为 tag,避免冲突
TIMESTAMP=$(date +%Y%m%d%H%M%S)
TAG="v${TIMESTAMP}"
echo "Creating timestamp tag: $TAG"
git tag -a "$TAG" -m "Release $TAG"
git push origin "$TAG"
echo "tag=$TAG" >> $GITHUB_OUTPUT
else
TAG="${{ github.ref_name }}"
echo "Using existing tag: $TAG"
echo "tag=$TAG" >> $GITHUB_OUTPUT
fi

- name: Generate changelog
id: generate_changelog
run: |
npx changelogithub --output ${{ github.workspace }}-CHANGELOG.txt || echo "" > ${{ github.workspace }}-CHANGELOG.txt

- name: Build Release
run: |
chmod +x build.sh
./build.sh --release --compress
env:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

- name: Move regular build
run: |
Expand All @@ -141,56 +64,33 @@ jobs:
run: |
chmod +x build.sh
./build.sh --release --compress --lite
env:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

- name: Move lite build and restore regular build
run: |
mkdir -p lite-dist
mv dist/* lite-dist/
mv regular-dist/* dist/

- name: Upload Release Assets
- name: Download i18n.tar.gz from latest release
run: |
# Delete local tag, or gh cli will complain about it.
git tag -d ${{ steps.get_current_tag.outputs.current_tag }}
gh release create \
--title "Release ${{ steps.get_current_tag.outputs.current_tag }}" \
--notes-file "${{ github.workspace }}-CHANGELOG.txt" \
--prerelease=${{ steps.check_pre_release.outputs.is_pre_release }} \
${{ steps.get_current_tag.outputs.current_tag }} \
dist/openlist-frontend-dist-v*.tar.gz lite-dist/openlist-frontend-dist-lite-v*.tar.gz dist/i18n.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
echo "Downloading i18n.tar.gz from latest release..."
curl -L -o dist/i18n.tar.gz "https://github.com/OpenListTeam/OpenList-Frontend/releases/latest/download/i18n.tar.gz" || echo "Failed to download i18n.tar.gz"
ls -la dist/i18n.tar.gz 2>/dev/null || echo "No i18n.tar.gz downloaded"

- name: Prepare for npm
- name: Delete existing release (if any)
run: |
# Delete the generated dist tarball
rm -f dist/openlist-frontend-dist-v*.tar.gz
rm -f lite-dist/openlist-frontend-dist-lite-v*.tar.gz
# Copy the lite version
mkdir dist/lite
cp -r lite-dist/. dist/lite/

if ! jq -e '.name and .version' package.json > /dev/null; then
echo "Error: Invalid package.json"
exit 1
if gh release view ${{ steps.tag.outputs.tag }} --json isDraft --quiet 2>/dev/null; then
echo "Deleting existing release: ${{ steps.tag.outputs.tag }}"
gh release delete ${{ steps.tag.outputs.tag }} --cleanup-tag=false --yes 2>/dev/null || true
fi

- name: Publish npm
- name: Upload Release Assets
run: |
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > ~/.npmrc

if [ -z "${{ secrets.NPM_TOKEN }}" ]; then
echo "NPM_TOKEN not set, performing dry run"
pnpm publish --dry-run --no-git-checks --access public
else
echo "Publishing to npm..."
pnpm publish --no-git-checks --access public
fi
gh release create \
--title "Release ${{ steps.tag.outputs.tag }}" \
${{ steps.tag.outputs.tag }} \
dist/openlist-frontend-dist-v*.tar.gz \
lite-dist/openlist-frontend-dist-lite-v*.tar.gz \
dist/i18n.tar.gz
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

permissions:
contents: write
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3 changes: 0 additions & 3 deletions .github/workflows/build_rolling.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,6 @@ jobs:
run: |
chmod +x build.sh
./build.sh --dev --compress
env:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

- name: Read version and determine tag name
id: version
Expand Down
7 changes: 0 additions & 7 deletions .github/workflows/i18n_sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,5 @@ jobs:
git push
fi
- name: Sync to Crowdin
if: ${{ steps.verify-changed-files.outputs.changed == 'true' || github.event_name == 'push' }}
uses: ./.github/actions/sync_to_crowdin
with:
crowdin_project_id: ${{ secrets.CROWDIN_PROJECT_ID }}
crowdin_personal_token: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

permissions:
contents: write
21 changes: 10 additions & 11 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,8 @@ check_git_version_and_commit() {
# Enforce git tag for release builds
enforce_git_tag() {
if ! git_version=$(git describe --abbrev=0 --tags 2>/dev/null); then
log_error "No git tags found. Release build requires a git tag."
log_warning "Please create a tag first, or use --dev for development builds."
exit 1
log_warning "No git tags found. Using version from package.json."
git_version="v$(grep '"version":' package.json | sed 's/.*"version": *"\([^"]*\)".*/\1/')"
fi
validate_git_tag
}
Expand All @@ -106,6 +105,13 @@ enforce_git_tag() {
validate_git_tag() {
package_version=$(grep '"version":' package.json | sed 's/.*"version": *"\([^"]*\)".*/\1/')
git_version_clean=${git_version#v}

# 检查是否为时间戳格式(纯数字),如果是则跳过版本验证
if [[ "$git_version_clean" =~ ^[0-9]+$ ]]; then
log_info "Timestamp tag detected (${git_version_clean}), skipping version validation"
return 0
fi

if [[ "$git_version_clean" != "$package_version" ]]; then
log_error "Package.json version (${package_version}) does not match git tag (${git_version_clean})."
exit 1
Expand Down Expand Up @@ -146,14 +152,7 @@ build_project() {
log_step "==== Installing dependencies ===="
pnpm install

log_step "==== Building i18n ===="
if [[ "$SKIP_I18N" == "false" ]]; then
pnpm i18n:release
else
fetch_i18n_from_release
fi

log_step "==== Building project ===="
log_step "==== Building project (English only, no crowdin) ===="
if [[ "$LITE_FLAG" == "true" ]]; then
pnpm build:lite
else
Expand Down
5 changes: 0 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,6 @@
],
"homepage": "https://openlist.team/",
"scripts": {
"crowdin:upload": "crowdin upload sources --auto-update",
"crowdin:download": "crowdin download --verbose",
"crowdin": "pnpm crowdin:upload && pnpm crowdin:download",
"i18n:build": "pnpm crowdin && node ./scripts/i18n.mjs",
"i18n:release": "pnpm run crowdin:download && node ./scripts/i18n.mjs",
"start": "vite",
"dev": "vite --force",
"build": "vite build",
Expand Down
51 changes: 18 additions & 33 deletions src/app/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,36 @@ import * as i18n from "@solid-primitives/i18n"
import { createResource, createSignal } from "solid-js"
export { i18n }

// glob search by Vite
const langs = import.meta.glob("~/lang/*/index.json", {
eager: true,
import: "lang",
})

// all available languages
export const languages = Object.keys(langs).map((langPath) => {
const langCode = langPath.split("/")[3]
const langName = langs[langPath] as string
return { code: langCode, lang: langName }
})

// determine browser's default language
const userLang = navigator.language.toLowerCase()
const defaultLang =
languages.find((lang) => lang.code.toLowerCase() === userLang)?.code ||
languages.find(
(lang) => lang.code.toLowerCase().split("-")[0] === userLang.split("-")[0],
)?.code ||
"en"

// Get initial language from localStorage or fallback to defaultLang
export let initialLang = localStorage.getItem("lang") ?? defaultLang

if (!languages.some((lang) => lang.code === initialLang)) {
initialLang = defaultLang
// Only use English as the default language (no crowdin, single language mode)
const langs = {
"~/lang/en/index.json": "English",
}

// all available languages (only English)
export const languages = [{ code: "en", lang: "English" }]

// Always use English as the default language
const defaultLang = "en"

// Get initial language - always English
export let initialLang = "en"

// Type imports
// use `type` to not include the actual dictionary in the bundle
import type * as en from "~/lang/en/entry"

export type Lang = keyof typeof langs
export type Lang = "en"
export type RawDictionary = typeof en.dict
export type Dictionary = i18n.Flatten<RawDictionary>

// Fetch and flatten the dictionary
const fetchDictionary = async (locale: Lang): Promise<Dictionary> => {
// Fetch and flatten the dictionary (only English)
const fetchDictionary = async (_locale: Lang): Promise<Dictionary> => {
try {
const dict: RawDictionary = (await import(`~/lang/${locale}/entry.ts`)).dict
const dict: RawDictionary = (await import(`~/lang/en/entry.ts`)).dict
return i18n.flatten(dict) // Flatten dictionary for easier access to keys
} catch (err) {
console.error(`Error loading dictionary for locale: ${locale}`, err)
throw new Error(`Failed to load dictionary for ${locale}`)
console.error(`Error loading dictionary for locale: English`, err)
throw new Error(`Failed to load dictionary for English`)
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/components/ImageWithError.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import { createSignal, JSXElement, Show } from "solid-js"
export const ImageWithError = <C extends ElementType = "img">(
props: ImageProps<C> & {
fallbackErr?: JSXElement
onLoad?: () => void
},
) => {
const [err, setErr] = createSignal(false)
return (
<Show when={!err()} fallback={props.fallbackErr}>
<Image
{...props}
onLoad={props.onLoad}
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The onLoad prop is redundantly specified both in the spread operator and explicitly. Since props are spread with {...props}, the explicit onLoad={props.onLoad} is unnecessary. Remove the explicit onLoad line or restructure to avoid redundancy.

Suggested change
onLoad={props.onLoad}

Copilot uses AI. Check for mistakes.
onError={() => {
setErr(true)
}}
Expand Down
Loading