Compare commits

...
6 Commits
Author SHA1 Message Date
qwqcode d25beede91 fix(scripts): detect unpublished npm versions
Query the npm registry for the exact local version instead of comparing only with the latest dist-tag.

Report unpublished packages accurately and avoid spawning pnpm for each lookup.
2026-07-25 04:01:48 +08:00
qwqcode 16b690c8c0 fix(plugins): prepare unpublished packages for release
Declare compatible Artalk ranges and npm repository metadata for the pending plugin versions.

Correct ESM and CommonJS declaration exports, trim plugin-kit package contents, and validate package types in CI.
2026-07-25 04:01:43 +08:00
qwqcode 2036a0261e fix(ci): publish npm package with trusted OIDC
Replace the long-lived npm token with GitHub OIDC credentials, verify the release version, and retain pnpm packaging so the published tarball still includes the repository license.
2026-07-25 03:26:34 +08:00
qwqcode 66c8f6b94b fix(docker): push release images by digest
Avoid assigning temporary platform tags to digest-only outputs so Buildx can push canonical per-platform digests before the manifest merge.
2026-07-25 03:26:33 +08:00
qwqcode 1bf21404bd fix(build): bind Docker metadata to release commit
Pass the resolved release commit into Docker builds, verify every checkout against it, and derive image version metadata and sha tags from the packaged source instead of the workflow invocation commit.
2026-07-24 14:24:00 +08:00
qwqcode 9ec0f1c74a fix(ci): pin release retries to reviewed commits
Resolve automatic releases from the merged release PR commit and require an explicit immutable commit when recovering a missing tag. Reuse existing tags idempotently, restore annotated release tags, and reject duplicate release preparation or inconsistent version files.
2026-07-24 14:23:26 +08:00
19 changed files with 540 additions and 104 deletions

No files matched your search

+44 -4
View File
@@ -9,6 +9,9 @@ on:
ref:
required: true
type: string
commit:
required: true
type: string
dry_run:
required: true
type: boolean
@@ -36,6 +39,20 @@ jobs:
ref: ${{ inputs.ref }}
persist-credentials: false
- name: Verify release source
env:
RELEASE_COMMIT: ${{ inputs.commit }}
run: |
if [[ ! "$RELEASE_COMMIT" =~ ^[0-9a-f]{40}$ ]]; then
echo "Invalid release commit: $RELEASE_COMMIT"
exit 1
fi
checkout_commit="$(git rev-parse HEAD)"
if [[ "$checkout_commit" != "$RELEASE_COMMIT" ]]; then
echo "Checked out $checkout_commit, expected $RELEASE_COMMIT."
exit 1
fi
- name: Setup pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
@@ -85,6 +102,20 @@ jobs:
ref: ${{ inputs.ref }}
persist-credentials: false
- name: Verify release source
env:
RELEASE_COMMIT: ${{ inputs.commit }}
run: |
if [[ ! "$RELEASE_COMMIT" =~ ^[0-9a-f]{40}$ ]]; then
echo "Invalid release commit: $RELEASE_COMMIT"
exit 1
fi
checkout_commit="$(git rev-parse HEAD)"
if [[ "$checkout_commit" != "$RELEASE_COMMIT" ]]; then
echo "Checked out $checkout_commit, expected $RELEASE_COMMIT."
exit 1
fi
- name: Download UI assets
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
@@ -119,7 +150,7 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
build-args: |
APP_VERSION=${{ inputs.version }}
APP_COMMIT_HASH=${{ github.sha }}
APP_COMMIT_HASH=${{ inputs.commit }}
SKIP_UI_BUILD=true
outputs: type=docker
cache-from: type=gha,scope=release-${{ matrix.platform_pair }}
@@ -134,10 +165,9 @@ jobs:
file: ./Dockerfile
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
tags: ${{ steps.meta.outputs.tags }}
build-args: |
APP_VERSION=${{ inputs.version }}
APP_COMMIT_HASH=${{ github.sha }}
APP_COMMIT_HASH=${{ inputs.commit }}
SKIP_UI_BUILD=true
outputs: type=image,push=true,name=${{ env.DOCKER_IMG }},push-by-digest=true,name-canonical=true
cache-from: type=gha,scope=release-${{ matrix.platform_pair }}
@@ -197,6 +227,16 @@ jobs:
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Prepare release metadata
env:
RELEASE_COMMIT: ${{ inputs.commit }}
run: |
if [[ ! "$RELEASE_COMMIT" =~ ^[0-9a-f]{40}$ ]]; then
echo "Invalid release commit: $RELEASE_COMMIT"
exit 1
fi
echo "RELEASE_COMMIT_SHORT=${RELEASE_COMMIT:0:7}" >> "$GITHUB_ENV"
- name: Generate Docker metadata
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
@@ -205,7 +245,7 @@ jobs:
tags: |
type=semver,pattern={{version}},value=${{ inputs.version }}
type=semver,pattern={{major}}.{{minor}},value=${{ inputs.version }}
type=sha
type=raw,value=sha-${{ env.RELEASE_COMMIT_SHORT }}
- name: Login to DockerHub
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
+172 -28
View File
@@ -13,6 +13,11 @@ on:
required: false
type: string
default: ""
release_commit:
description: "Immutable commit to tag when the release tag does not exist"
required: false
type: string
default: ""
outputs:
version:
description: "Version"
@@ -20,6 +25,9 @@ on:
ref:
description: "Git ref to build"
value: ${{ jobs.tagging.outputs.ref }}
commit:
description: "Release source commit"
value: ${{ jobs.tagging.outputs.commit }}
permissions:
contents: write
@@ -52,28 +60,31 @@ jobs:
echo "Version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Checkout Code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Create Tag
if: ${{ !inputs.dry_run }}
- name: Resolve release target
id: target
env:
DEFAULT_COMMIT: ${{ github.sha }}
DRY_RUN: ${{ inputs.dry_run }}
GH_TOKEN: ${{ github.token }}
REQUESTED_COMMIT: ${{ inputs.release_commit }}
VERSION: ${{ steps.version.outputs.version }}
run: |
resolve_tag_commit() {
local object_type="$1"
local object_sha="$2"
local depth=0
if [[ "$object_type" == "tag" ]]; then
while [[ "$object_type" == "tag" ]]; do
depth=$((depth + 1))
if (( depth > 8 )); then
echo "Tag $VERSION has too many nested tag objects." >&2
return 1
fi
IFS=$'\t' read -r object_type object_sha < <(
gh api "repos/$GITHUB_REPOSITORY/git/tags/$object_sha" \
--jq '.object | [.type, .sha] | @tsv'
)
fi
done
if [[ "$object_type" != "commit" ]]; then
echo "Tag $VERSION resolves to unsupported object type $object_type." >&2
@@ -83,31 +94,163 @@ jobs:
printf '%s\n' "$object_sha"
}
release_sha="$(git rev-parse HEAD)"
requested_commit="$(tr '[:upper:]' '[:lower:]' <<< "$REQUESTED_COMMIT")"
if [[ -n "$requested_commit" && ! "$requested_commit" =~ ^[0-9a-f]{40}$ ]]; then
echo "release_commit must be a full 40-character commit SHA."
exit 1
fi
tag_ref="$(
gh api "repos/$GITHUB_REPOSITORY/git/matching-refs/tags/$VERSION" \
--jq ".[] | select(.ref == \"refs/tags/$VERSION\") | [.object.type, .object.sha] | @tsv"
)"
if [[ -z "$tag_ref" ]]; then
gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" \
-f ref="refs/tags/$VERSION" \
-f sha="$release_sha" > /dev/null
tag_ref="$(
gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$VERSION" \
--jq '.object | [.type, .sha] | @tsv'
)"
echo "Created tag $VERSION at $release_sha."
fi
IFS=$'\t' read -r tag_type tag_sha <<< "$tag_ref"
tag_commit="$(resolve_tag_commit "$tag_type" "$tag_sha")"
if [[ "$tag_commit" != "$release_sha" ]]; then
echo "Tag $VERSION points to $tag_commit, expected $release_sha."
tag_exists=false
if [[ -n "$tag_ref" ]]; then
IFS=$'\t' read -r tag_type tag_sha <<< "$tag_ref"
release_commit="$(resolve_tag_commit "$tag_type" "$tag_sha")"
tag_exists=true
if [[ -n "$requested_commit" && "$release_commit" != "$requested_commit" ]]; then
echo "Tag $VERSION points to $release_commit, not requested commit $requested_commit."
exit 1
fi
elif [[ -n "$requested_commit" ]]; then
release_commit="$requested_commit"
elif [[ "$DRY_RUN" == "true" ]]; then
release_commit="$DEFAULT_COMMIT"
else
echo "Tag $VERSION does not exist and release_commit was not provided."
echo "Use the reviewed release pull request merge commit for a production retry."
exit 1
fi
echo "Verified tag $VERSION at $release_sha."
verified_commit="$(
gh api "repos/$GITHUB_REPOSITORY/git/commits/$release_commit" --jq '.sha'
)"
if [[ "$verified_commit" != "$release_commit" ]]; then
echo "Unable to verify release commit $release_commit."
exit 1
fi
echo "commit=$release_commit" >> "$GITHUB_OUTPUT"
echo "tag_exists=$tag_exists" >> "$GITHUB_OUTPUT"
- name: Checkout Code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ steps.target.outputs.commit }}
fetch-depth: 0
persist-credentials: false
- name: Validate release source
env:
RELEASE_COMMIT: ${{ steps.target.outputs.commit }}
VERSION: ${{ steps.version.outputs.version }}
run: |
checkout_commit="$(git rev-parse HEAD)"
if [[ "$checkout_commit" != "$RELEASE_COMMIT" ]]; then
echo "Checked out $checkout_commit, expected $RELEASE_COMMIT."
exit 1
fi
git fetch --no-tags origin \
"+refs/heads/master:refs/remotes/origin/master"
if ! git merge-base --is-ancestor "$RELEASE_COMMIT" origin/master; then
echo "Release commit $RELEASE_COMMIT is not reachable from master."
exit 1
fi
package_version="${VERSION#v}"
app_version="$(
sed -n 's/^var Version = "\(v[0-9][0-9.]*\)"$/\1/p' internal/config/version.go
)"
ui_version="$(jq -r '.version' ui/artalk/package.json)"
docs_version="$(jq -r '.latest' docs/docs/code/ArtalkVersion.json)"
if [[ "$app_version" != "$VERSION" ||
"$ui_version" != "$package_version" ||
"$docs_version" != "$package_version" ]]; then
echo "Release commit versions do not match $VERSION: app=$app_version ui=$ui_version docs=$docs_version."
exit 1
fi
- name: Create Tag
if: ${{ !inputs.dry_run }}
env:
GH_TOKEN: ${{ github.token }}
RELEASE_COMMIT: ${{ steps.target.outputs.commit }}
TAG_EXISTS: ${{ steps.target.outputs.tag_exists }}
VERSION: ${{ steps.version.outputs.version }}
run: |
resolve_tag_commit() {
local object_type="$1"
local object_sha="$2"
local depth=0
while [[ "$object_type" == "tag" ]]; do
depth=$((depth + 1))
if (( depth > 8 )); then
echo "Tag $VERSION has too many nested tag objects." >&2
return 1
fi
IFS=$'\t' read -r object_type object_sha < <(
gh api "repos/$GITHUB_REPOSITORY/git/tags/$object_sha" \
--jq '.object | [.type, .sha] | @tsv'
)
done
if [[ "$object_type" != "commit" ]]; then
echo "Tag $VERSION resolves to unsupported object type $object_type." >&2
return 1
fi
printf '%s\n' "$object_sha"
}
get_tag_ref() {
gh api "repos/$GITHUB_REPOSITORY/git/matching-refs/tags/$VERSION" \
--jq ".[] | select(.ref == \"refs/tags/$VERSION\") | [.object.type, .object.sha] | @tsv"
}
if [[ "$TAG_EXISTS" != "true" ]]; then
tag_object_sha="$(
gh api --method POST "repos/$GITHUB_REPOSITORY/git/tags" \
-f tag="$VERSION" \
-f message="Release $VERSION" \
-f object="$RELEASE_COMMIT" \
-f type=commit \
--jq '.sha'
)"
if ! create_error="$(
gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" \
-f ref="refs/tags/$VERSION" \
-f sha="$tag_object_sha" 2>&1
)"; then
tag_ref="$(get_tag_ref)"
if [[ -z "$tag_ref" ]]; then
printf '%s\n' "$create_error" >&2
echo "GitHub rejected tag $VERSION. Check the active tag rulesets; tag targets must not require linear history." >&2
exit 1
fi
echo "Tag $VERSION was created concurrently; verifying it."
else
echo "Created annotated tag $VERSION at $RELEASE_COMMIT."
fi
fi
tag_ref="$(get_tag_ref)"
if [[ -z "$tag_ref" ]]; then
echo "Tag $VERSION is missing after creation."
exit 1
fi
IFS=$'\t' read -r tag_type tag_sha <<< "$tag_ref"
tag_commit="$(resolve_tag_commit "$tag_type" "$tag_sha")"
if [[ "$tag_commit" != "$RELEASE_COMMIT" ]]; then
echo "Tag $VERSION points to $tag_commit, expected $RELEASE_COMMIT."
exit 1
fi
echo "Verified tag $VERSION at $RELEASE_COMMIT."
- name: Comment tag info to PR
uses: ./.github/actions/comment
@@ -118,4 +261,5 @@ jobs:
🔖 Tagged version [${{ steps.version.outputs.version }}](https://github.com/ArtalkJS/Artalk/tree/${{ steps.version.outputs.version }}) and updated the [CHANGELOG.md](https://github.com/ArtalkJS/Artalk/blob/master/CHANGELOG.md).
outputs:
version: ${{ steps.version.outputs.version }}
ref: ${{ inputs.dry_run && github.sha || steps.version.outputs.version }}
ref: ${{ inputs.dry_run && steps.target.outputs.commit || steps.version.outputs.version }}
commit: ${{ steps.target.outputs.commit }}
+22 -9
View File
@@ -12,9 +12,6 @@ on:
dry_run:
required: true
type: boolean
secrets:
NPM_TOKEN:
required: false
permissions:
contents: read
@@ -39,8 +36,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22.x
registry-url: https://registry.npmjs.org/
node-version: 24.x
package-manager-cache: false
- name: Install dependencies
@@ -49,15 +45,32 @@ jobs:
- name: Build
run: pnpm build
- name: Pack npm package
env:
VERSION: ${{ inputs.version }}
run: |
package_version="v$(node --print "require('./ui/artalk/package.json').version")"
if [[ "$package_version" != "$VERSION" ]]; then
echo "Package version $package_version does not match release version $VERSION."
exit 1
fi
pnpm --filter artalk pack --pack-destination "$RUNNER_TEMP"
package_file="$RUNNER_TEMP/artalk-${package_version#v}.tgz"
if [[ ! -f "$package_file" ]]; then
echo "Expected package tarball was not created: $package_file"
exit 1
fi
echo "PACKAGE_FILE=$package_file" >> "$GITHUB_ENV"
- name: Validate package
if: ${{ inputs.dry_run }}
run: pnpm publish -F artalk --no-git-checks --dry-run
run: npm publish "$PACKAGE_FILE" --dry-run --access public
- name: Publish
if: ${{ !inputs.dry_run }}
run: pnpm publish -F artalk --no-git-checks --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# npm validates build.yml, the calling workflow, for trusted publishing.
run: npm publish "$PACKAGE_FILE" --access public
# GitHub Packages
#
+7 -3
View File
@@ -1,5 +1,5 @@
name: Build and Publish
run-name: Build and Publish (${{ github.head_ref || github.ref }}) ${{ inputs.dry_run && '(🧪 Dry-Run)' || '' }}
run-name: Build and Publish (${{ inputs.version || github.head_ref || github.ref }}) ${{ inputs.dry_run && '(🧪 Dry-Run)' || '' }}
on:
pull_request:
@@ -15,6 +15,10 @@ on:
version:
description: "Release version (for example, v2.10.0)"
type: string
required: true
release_commit:
description: "Full release commit SHA (required when a production retry has no tag)"
type: string
required: false
build_items:
description: "Build items"
@@ -57,6 +61,7 @@ jobs:
issue_number: ${{ github.event.pull_request.number || 0 }}
dry_run: ${{ inputs.dry_run || false }}
version: ${{ inputs.version || '' }}
release_commit: ${{ github.event.pull_request.merge_commit_sha || inputs.release_commit || '' }}
# Build the UI
ui:
@@ -70,8 +75,6 @@ jobs:
permissions:
contents: read
id-token: write
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
# Build the Docker image
docker:
@@ -81,6 +84,7 @@ jobs:
with:
version: ${{ needs.tagging.outputs.version }}
ref: ${{ needs.tagging.outputs.ref }}
commit: ${{ needs.tagging.outputs.commit }}
dry_run: ${{ inputs.dry_run || false }}
permissions:
contents: read
+119
View File
@@ -45,6 +45,7 @@ jobs:
- name: Handle Version Number
env:
CUSTOM_VERSION: ${{ inputs.custom_version }}
GH_TOKEN: ${{ github.token }}
SEMVER_INCREMENT: ${{ inputs.semver }}
run: |
PREV_VERSION="$(git describe --tags --abbrev=0 --match 'v*')"
@@ -53,6 +54,31 @@ jobs:
exit 1
fi
if ! git merge-base --is-ancestor "$PREV_VERSION" HEAD; then
echo "Latest release tag $PREV_VERSION is not an ancestor of master."
exit 1
fi
CURRENT_APP_VERSION="$(
sed -n 's/^var Version = "\(v[0-9][0-9.]*\)"$/\1/p' internal/config/version.go
)"
CURRENT_UI_VERSION="$(jq -r '.version' ui/artalk/package.json)"
CURRENT_DOCS_VERSION="$(jq -r '.latest' docs/docs/code/ArtalkVersion.json)"
CURRENT_PACKAGE_VERSION="${CURRENT_APP_VERSION#v}"
if [[ -z "$CURRENT_APP_VERSION" ||
"$CURRENT_UI_VERSION" != "$CURRENT_PACKAGE_VERSION" ||
"$CURRENT_DOCS_VERSION" != "$CURRENT_PACKAGE_VERSION" ]]; then
echo "Release versions are inconsistent: app=$CURRENT_APP_VERSION ui=$CURRENT_UI_VERSION docs=$CURRENT_DOCS_VERSION."
exit 1
fi
if [[ "$CURRENT_APP_VERSION" != "$PREV_VERSION" ]]; then
echo "Source version $CURRENT_APP_VERSION does not match the latest release tag $PREV_VERSION."
echo "If the release PR was already merged, recover it with Build and Publish instead of preparing the release again."
exit 1
fi
if [[ -n "$CUSTOM_VERSION" ]]; then
NEXT_VERSION="$CUSTOM_VERSION"
else
@@ -72,6 +98,52 @@ jobs:
exit 1
fi
version_is_greater() {
local next_version="${1#v}"
local previous_version="${2#v}"
local -a next_parts previous_parts
local index next_part previous_part
IFS='.' read -r -a next_parts <<< "$next_version"
IFS='.' read -r -a previous_parts <<< "$previous_version"
for index in 0 1 2; do
next_part=$((10#${next_parts[$index]}))
previous_part=$((10#${previous_parts[$index]}))
if (( next_part > previous_part )); then
return 0
fi
if (( next_part < previous_part )); then
return 1
fi
done
return 1
}
if ! version_is_greater "$NEXT_VERSION" "$PREV_VERSION"; then
echo "Release version $NEXT_VERSION must be greater than $PREV_VERSION."
exit 1
fi
if git show-ref --verify --quiet "refs/tags/$NEXT_VERSION"; then
echo "Release tag $NEXT_VERSION already exists."
exit 1
fi
release_branch="release/$NEXT_VERSION"
existing_pr="$(
gh pr list \
--repo "$GITHUB_REPOSITORY" \
--head "$release_branch" \
--base master \
--state open \
--json number,url \
--jq 'if length == 0 then "" else "#\(.[0].number) \(.[0].url)" end'
)"
if [[ -n "$existing_pr" ]]; then
echo "Release pull request $existing_pr already exists for $NEXT_VERSION."
exit 1
fi
echo "PREV_VERSION=${PREV_VERSION}" >> "$GITHUB_ENV"
echo "VERSION=${NEXT_VERSION}" >> "$GITHUB_ENV"
@@ -110,6 +182,53 @@ jobs:
PACKAGE_VERSION="$PACKAGE_VERSION" perl -pi -e \
's#"latest"(\W+)?:(\W+)?".*?"#"latest": "$ENV{PACKAGE_VERSION}"#g' docs/docs/code/ArtalkVersion.json
- name: Validate Release Changes
run: |
PACKAGE_VERSION="${VERSION#v}"
APP_VERSION="$(
sed -n 's/^var Version = "\(v[0-9][0-9.]*\)"$/\1/p' internal/config/version.go
)"
UI_VERSION="$(jq -r '.version' ui/artalk/package.json)"
DOCS_VERSION="$(jq -r '.latest' docs/docs/code/ArtalkVersion.json)"
if [[ "$APP_VERSION" != "$VERSION" ||
"$UI_VERSION" != "$PACKAGE_VERSION" ||
"$DOCS_VERSION" != "$PACKAGE_VERSION" ]]; then
echo "Generated release versions are inconsistent: app=$APP_VERSION ui=$UI_VERSION docs=$DOCS_VERSION."
exit 1
fi
changelog_entries="$(grep -cF "## [$VERSION]" CHANGELOG.md || true)"
if [[ "$changelog_entries" != "1" ]]; then
echo "Expected one CHANGELOG entry for $VERSION, found $changelog_entries."
exit 1
fi
first_changelog_version="$(
sed -n 's/^## \[\(v[0-9][^]]*\)\].*/\1/p' CHANGELOG.md | head -n 1
)"
if [[ "$first_changelog_version" != "$VERSION" ]]; then
echo "The first CHANGELOG release is $first_changelog_version, expected $VERSION."
exit 1
fi
expected_changes="$(
printf '%s\n' \
CHANGELOG.md \
docs/docs/code/ArtalkVersion.json \
internal/config/version.go \
ui/artalk/package.json |
sort
)"
actual_changes="$(git diff --name-only | sort)"
if [[ "$actual_changes" != "$expected_changes" ]]; then
echo "Unexpected release file set:"
git diff --name-only
exit 1
fi
git diff --check
- name: Print Git Diff
run: git diff
+8
View File
@@ -70,6 +70,14 @@ jobs:
if: ${{ matrix.quality }}
run: pnpm build:plugins
- name: Plugins Type Test
if: ${{ matrix.quality }}
run: pnpm test:plugins
- name: Plugins Package Test
if: ${{ matrix.quality }}
run: pnpm test:plugins:attw
- name: Lint
if: ${{ matrix.quality }}
run: pnpm eslint ui && pnpm prettier --check ui
+2
View File
@@ -40,6 +40,8 @@ export default defineConfig(
'**/*.config.js',
'**/*.config.ts',
'**/*.d.ts',
'**/*.d.cts',
'**/*.d.mts',
],
},
+2
View File
@@ -19,6 +19,8 @@
"build:auth": "pnpm -F @artalk/plugin-auth build",
"build:plugin-kit": "pnpm -F @artalk/plugin-kit build",
"build:eslint-plugin": "pnpm -F eslint-plugin-artalk build",
"test:plugins": "pnpm -F @artalk/plugin-* exec tsc --noEmit && pnpm -F eslint-plugin-artalk exec tsc --noEmit && pnpm -F eslint-plugin-artalk test --run",
"test:plugins:attw": "pnpm -F @artalk/plugin-* -F eslint-plugin-artalk test:attw",
"build:typedoc": "pnpm typedoc",
"build:docs": "pnpm build && pnpm build:plugins && pnpm -F=docs-landing build && pnpm -F=docs-swagger swagger:build && pnpm -F=docs build:docs && pnpm after:build-docs",
"after:build-docs": "cp -rf docs/landing/dist/* docs/swagger/dist/* docs/docs/.vitepress/dist && pnpm build:typedoc && pnpm fetch:registry",
+50 -32
View File
@@ -1,15 +1,34 @@
#!/usr/bin/env node
import { promises as fs, readFileSync } from 'fs'
import path from 'path'
import { execFileSync } from 'child_process'
import process from 'process'
import { promises as fs, readFileSync } from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
const __dirname = path.dirname(new URL(import.meta.url).pathname)
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const REGISTRY_URL = 'https://registry.npmjs.org/'
const REQUEST_TIMEOUT = 10_000
// Helper to run shell commands
const getPublishedVersion = (packageName) =>
execFileSync('pnpm', ['info', packageName, 'version'], { encoding: 'utf-8' }).trim()
const getPublishedPackage = async (packageName) => {
const packageUrl = new URL(encodeURIComponent(packageName), REGISTRY_URL)
const response = await fetch(packageUrl, {
headers: { Accept: 'application/vnd.npm.install-v1+json' },
signal: AbortSignal.timeout(REQUEST_TIMEOUT),
})
if (response.status === 404) {
return { latestVersion: null, versions: new Set() }
}
if (!response.ok) {
throw new Error(`npm registry returned HTTP ${response.status} for ${packageName}`)
}
const packageMetadata = await response.json()
return {
latestVersion: packageMetadata['dist-tags']?.latest || null,
versions: new Set(Object.keys(packageMetadata.versions || {})),
}
}
// Recursively find all directories containing package.json
const findNodeProjects = async (dir) => {
@@ -36,8 +55,8 @@ const findNodeProjects = async (dir) => {
return projects
}
// Compare local version with the latest npm version
const checkVersionMismatch = async (projectPath) => {
// Check whether the exact local version has already been published to npm.
const checkVersionPublished = async (projectPath) => {
const packageJsonPath = path.join(projectPath, 'package.json')
try {
@@ -46,23 +65,22 @@ const checkVersionMismatch = async (projectPath) => {
const localVersion = packageJson.version
const packageName = packageJson.name
// Get the latest version from npm using pnpm info
const npmVersion = getPublishedVersion(packageName)
const { latestVersion, versions } = await getPublishedPackage(packageName)
if (localVersion === npmVersion) {
console.log(`✅ ${packageName} is up to date (${npmVersion})`)
} else {
console.log(`❌ ${packageName} is outdated (local: ${localVersion}, npm: ${npmVersion})`)
if (versions.has(localVersion)) {
const latestSuffix =
localVersion === latestVersion ? '' : `; npm latest is ${latestVersion || 'unset'}`
console.log(`✅ ${packageName}@${localVersion} is published${latestSuffix}`)
return null
}
if (localVersion !== npmVersion) {
return { packageName, localVersion, latestVersion: npmVersion }
}
console.log(
`❌ ${packageName}@${localVersion} is not published (npm latest: ${latestVersion || 'none'})`,
)
return { packageName, localVersion, latestVersion }
} catch (error) {
throw new Error(`Failed to check npm version for ${projectPath}`, { cause: error })
}
return null
}
// Parse command-line arguments to get the project name if provided
@@ -79,8 +97,8 @@ const getArgs = () => {
return specifiedProject
}
// Main function to find outdated packages
const findOutdatedProjects = async () => {
// Main function to find unpublished local package versions
const findUnpublishedProjects = async () => {
const specifiedProject = getArgs()
let projects = await findNodeProjects(path.join(__dirname, '../ui'))
console.log(`Found ${projects.length} projects under 'ui' directory.\n`)
@@ -100,29 +118,29 @@ const findOutdatedProjects = async () => {
}
}
const outdatedProjects = []
const unpublishedProjects = []
for (const project of projects) {
const result = await checkVersionMismatch(project)
const result = await checkVersionPublished(project)
if (result) {
outdatedProjects.push(result)
unpublishedProjects.push(result)
}
}
console.log('\n==================================================\n')
if (outdatedProjects.length === 0) {
console.log('✅ All projects have the latest versions pushed to npm.')
if (unpublishedProjects.length === 0) {
console.log('✅ Every local package version is published to npm.')
} else {
console.log('Projects with outdated versions:\n')
outdatedProjects.forEach(({ packageName, localVersion, latestVersion }) => {
console.log(`❌ ${packageName}: Local version ${localVersion}, NPM version ${latestVersion}`)
console.log('Local package versions not published to npm:\n')
unpublishedProjects.forEach(({ packageName, localVersion, latestVersion }) => {
console.log(`❌ ${packageName}: local ${localVersion}, npm latest ${latestVersion || 'none'}`)
})
process.exit(1)
}
}
findOutdatedProjects().catch((error) => {
findUnpublishedProjects().catch((error) => {
console.error(error)
process.exitCode = 1
})
+8 -1
View File
@@ -4,6 +4,12 @@
"type": "module",
"license": "MIT",
"homepage": "https://github.com/ArtalkJS/Artalk/tree/master/ui/eslint-plugin-artalk",
"bugs": "https://github.com/ArtalkJS/Artalk/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/ArtalkJS/Artalk.git",
"directory": "ui/eslint-plugin-artalk"
},
"files": [
"dist"
],
@@ -24,7 +30,8 @@
"scripts": {
"dev": "tsdown --watch",
"build": "tsdown",
"test": "vitest"
"test": "vitest",
"test:attw": "pnpx @arethetypeswrong/cli@0.18.5 --pack ."
},
"dependencies": {
"@typescript-eslint/utils": "^8.65.0"
+22 -5
View File
@@ -1,8 +1,19 @@
{
"name": "@artalk/plugin-auth",
"version": "0.0.3",
"minAppVersion": "2.10.0",
"license": "LGPL-3.0-only",
"description": "Auth plugin for artalk",
"homepage": "https://github.com/ArtalkJS/Artalk/tree/master/ui/plugin-auth",
"bugs": "https://github.com/ArtalkJS/Artalk/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/ArtalkJS/Artalk.git",
"directory": "ui/plugin-auth"
},
"publishConfig": {
"access": "public"
},
"type": "module",
"files": [
"dist"
@@ -12,15 +23,21 @@
"types": "./dist/artalk-plugin-auth.d.ts",
"exports": {
".": {
"types": "./dist/artalk-plugin-auth.d.ts",
"import": "./dist/artalk-plugin-auth.mjs",
"require": "./dist/artalk-plugin-auth.cjs"
"import": {
"types": "./dist/artalk-plugin-auth.d.ts",
"default": "./dist/artalk-plugin-auth.mjs"
},
"require": {
"types": "./dist/artalk-plugin-auth.d.cts",
"default": "./dist/artalk-plugin-auth.cjs"
}
}
},
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview"
"serve": "vite preview",
"test:attw": "pnpx @arethetypeswrong/cli@0.18.5 --pack ."
},
"devDependencies": {
"@artalk/plugin-kit": "workspace:^",
@@ -30,6 +47,6 @@
"vite-plugin-solid": "^2.11.13"
},
"peerDependencies": {
"artalk": "workspace:^"
"artalk": "^2.10.0"
}
}
@@ -1,3 +0,0 @@
import type { ArtalkPlugin } from 'artalk'
declare const ArtalkAuthPlugin: ArtalkPlugin
-3
View File
@@ -1,3 +0,0 @@
import type { ArtalkPlugin } from 'artalk'
declare const ArtalkAuthPlugin: ArtalkPlugin
+22 -5
View File
@@ -1,8 +1,19 @@
{
"name": "@artalk/plugin-katex",
"version": "0.2.5",
"minAppVersion": "2.8.6",
"license": "MIT",
"description": "The katex plugin for artalk",
"homepage": "https://github.com/ArtalkJS/Artalk/tree/master/ui/plugin-katex",
"bugs": "https://github.com/ArtalkJS/Artalk/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/ArtalkJS/Artalk.git",
"directory": "ui/plugin-katex"
},
"publishConfig": {
"access": "public"
},
"type": "module",
"main": "./dist/artalk-plugin-katex.js",
"module": "./dist/artalk-plugin-katex.mjs",
@@ -13,7 +24,8 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview"
"serve": "vite preview",
"test:attw": "pnpx @arethetypeswrong/cli@0.18.5 --pack ."
},
"devDependencies": {
"@artalk/plugin-kit": "workspace:^",
@@ -22,14 +34,19 @@
"marked": "^18.0.7"
},
"peerDependencies": {
"artalk": "workspace:^",
"artalk": ">=2.8.6 <3.0.0",
"katex": ">=0.16.45 <0.18.0"
},
"exports": {
".": {
"types": "./dist/artalk-plugin-katex.d.ts",
"import": "./dist/artalk-plugin-katex.mjs",
"require": "./dist/artalk-plugin-katex.cjs"
"import": {
"types": "./dist/artalk-plugin-katex.d.ts",
"default": "./dist/artalk-plugin-katex.mjs"
},
"require": {
"types": "./dist/artalk-plugin-katex.d.cts",
"default": "./dist/artalk-plugin-katex.cjs"
}
}
}
}
+9
View File
@@ -0,0 +1,9 @@
import Artalk from 'artalk'
export {}
declare global {
interface Window {
Artalk?: typeof Artalk
}
}
+1 -2
View File
@@ -1,10 +1,9 @@
import Artalk, { ArtalkPlugin } from 'artalk'
import Artalk from 'artalk'
export {}
declare global {
interface Window {
Artalk?: typeof Artalk
ArtalkPlugins?: { [name: string]: ArtalkPlugin }
}
}
+30 -3
View File
@@ -2,6 +2,16 @@
"name": "@artalk/plugin-kit",
"version": "1.0.9",
"description": "The plugin kit for Artalk",
"homepage": "https://github.com/ArtalkJS/Artalk/tree/master/ui/plugin-kit",
"bugs": "https://github.com/ArtalkJS/Artalk/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/ArtalkJS/Artalk.git",
"directory": "ui/plugin-kit"
},
"publishConfig": {
"access": "public"
},
"type": "module",
"main": "dist/main.cjs",
"types": "dist/main.d.cts",
@@ -16,15 +26,32 @@
"default": "./dist/main.mjs"
}
},
"./client": "./client.d.ts"
"./client": {
"import": {
"types": "./client.d.ts"
},
"require": {
"types": "./client.d.cts"
}
}
},
"scripts": {
"dev": "tsdown --watch",
"build": "tsdown"
"build": "tsdown",
"test:attw": "pnpx @arethetypeswrong/cli@0.18.5 --pack ."
},
"keywords": [],
"author": "",
"license": "MIT",
"files": [
"dist/**/*.cjs",
"dist/**/*.mjs",
"dist/**/*.map",
"dist/**/*.d.cts",
"dist/**/*.d.mts",
"client.d.ts",
"client.d.cts"
],
"dependencies": {
"@microsoft/api-extractor": "^7.58.12",
"picocolors": "^1.1.1"
@@ -35,7 +62,7 @@
"vite": "^8.1.5"
},
"peerDependencies": {
"artalk": "workspace:^",
"artalk": "^2.10.0",
"typescript": "*",
"vite": "*"
}
-1
View File
@@ -1,5 +1,4 @@
interface Window {
lightGallery: (el: any, options: any) => any | undefined
Fancybox?: any
lightbox?: any
PhotoSwipeLightbox?: any
+22 -5
View File
@@ -1,8 +1,19 @@
{
"name": "@artalk/plugin-lightbox",
"version": "0.2.5",
"minAppVersion": "2.9.1",
"license": "MIT",
"description": "The image lightbox plugin for artalk",
"homepage": "https://github.com/ArtalkJS/Artalk/tree/master/ui/plugin-lightbox",
"bugs": "https://github.com/ArtalkJS/Artalk/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/ArtalkJS/Artalk.git",
"directory": "ui/plugin-lightbox"
},
"publishConfig": {
"access": "public"
},
"type": "module",
"main": "./dist/artalk-plugin-lightbox.js",
"module": "./dist/artalk-plugin-lightbox.mjs",
@@ -13,7 +24,8 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview"
"serve": "vite preview",
"test:attw": "pnpx @arethetypeswrong/cli@0.18.5 --pack ."
},
"devDependencies": {
"@artalk/plugin-kit": "workspace:^",
@@ -24,7 +36,7 @@
"photoswipe": "^5.4.4"
},
"peerDependencies": {
"artalk": "workspace:^",
"artalk": "^2.9.1",
"fancybox": "^3.0.1",
"lightbox2": "^2.12.0",
"lightgallery": "^2.9.0",
@@ -46,9 +58,14 @@
},
"exports": {
".": {
"types": "./dist/artalk-plugin-lightbox.d.ts",
"import": "./dist/artalk-plugin-lightbox.mjs",
"require": "./dist/artalk-plugin-lightbox.cjs"
"import": {
"types": "./dist/artalk-plugin-lightbox.d.ts",
"default": "./dist/artalk-plugin-lightbox.mjs"
},
"require": {
"types": "./dist/artalk-plugin-lightbox.d.cts",
"default": "./dist/artalk-plugin-lightbox.cjs"
}
}
}
}