Compare commits

..
1 Commits
Author SHA1 Message Date
qwqcode 3fde057a44 feat(build): trigger COPR RPM builds on release
Add a least-privilege reusable workflow that validates release versions and assets before requesting a COPR build. Keep dry runs side-effect free, send the exact release metadata as JSON, wait for application publishing when selected, and include RPM status in the release result.
2026-07-24 00:24:47 +08:00
20 changed files with 186 additions and 531 deletions

No files matched your search

+4 -44
View File
@@ -9,9 +9,6 @@ on:
ref:
required: true
type: string
commit:
required: true
type: string
dry_run:
required: true
type: boolean
@@ -39,20 +36,6 @@ 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:
@@ -102,20 +85,6 @@ 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:
@@ -150,7 +119,7 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
build-args: |
APP_VERSION=${{ inputs.version }}
APP_COMMIT_HASH=${{ inputs.commit }}
APP_COMMIT_HASH=${{ github.sha }}
SKIP_UI_BUILD=true
outputs: type=docker
cache-from: type=gha,scope=release-${{ matrix.platform_pair }}
@@ -165,9 +134,10 @@ 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=${{ inputs.commit }}
APP_COMMIT_HASH=${{ github.sha }}
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 }}
@@ -227,16 +197,6 @@ 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
@@ -245,7 +205,7 @@ jobs:
tags: |
type=semver,pattern={{version}},value=${{ inputs.version }}
type=semver,pattern={{major}}.{{minor}},value=${{ inputs.version }}
type=raw,value=sha-${{ env.RELEASE_COMMIT_SHORT }}
type=sha
- name: Login to DockerHub
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
+72
View File
@@ -0,0 +1,72 @@
name: Trigger RPM Build
on:
workflow_call:
inputs:
version:
required: true
type: string
dry_run:
required: true
type: boolean
secrets:
COPR_BUILD_WEBHOOK_URL:
required: false
permissions: {}
jobs:
trigger_copr:
runs-on: ubuntu-latest
steps:
- name: Validate release version
env:
VERSION: ${{ inputs.version }}
run: |
if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Invalid release version: $VERSION"
exit 1
fi
- name: Print dry-run request
if: ${{ inputs.dry_run }}
env:
VERSION: ${{ inputs.version }}
run: echo "Would request the COPR build for $VERSION."
- name: Verify release assets
if: ${{ !inputs.dry_run }}
env:
VERSION: ${{ inputs.version }}
run: |
curl --fail --silent --show-error --location --head \
--proto '=https' --proto-redir '=https' \
--connect-timeout 15 --max-time 60 \
"https://github.com/$GITHUB_REPOSITORY/releases/download/$VERSION/artalk_ui.tar.gz"
- name: Trigger Copr Build Webhook
if: ${{ !inputs.dry_run }}
env:
COPR_BUILD_WEBHOOK_URL: ${{ secrets.COPR_BUILD_WEBHOOK_URL }}
VERSION: ${{ inputs.version }}
run: |
if [[ -z "$COPR_BUILD_WEBHOOK_URL" ]]; then
echo "COPR_BUILD_WEBHOOK_URL is not configured."
exit 1
fi
payload="$(
jq -cn \
--arg repository "$GITHUB_REPOSITORY" \
--arg ref "refs/tags/$VERSION" \
--arg version "$VERSION" \
'{repository: $repository, ref: $ref, version: $version}'
)"
curl --fail-with-body --silent --show-error \
--proto '=https' \
--connect-timeout 15 --max-time 60 \
--header 'Content-Type: application/json' \
--request POST \
--data "$payload" \
"${COPR_BUILD_WEBHOOK_URL%/}/artalk/"
+18 -162
View File
@@ -13,11 +13,6 @@ 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"
@@ -25,9 +20,6 @@ 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
@@ -60,143 +52,28 @@ jobs:
echo "Version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- 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
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"
}
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"
)"
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
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
if [[ "$object_type" == "tag" ]]; then
IFS=$'\t' read -r object_type object_sha < <(
gh api "repos/$GITHUB_REPOSITORY/git/tags/$object_sha" \
--jq '.object | [.type, .sha] | @tsv'
)
done
fi
if [[ "$object_type" != "commit" ]]; then
echo "Tag $VERSION resolves to unsupported object type $object_type." >&2
@@ -206,51 +83,31 @@ jobs:
printf '%s\n' "$object_sha"
}
get_tag_ref() {
release_sha="$(git rev-parse HEAD)"
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
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_COMMIT" ]]; then
echo "Tag $VERSION points to $tag_commit, expected $RELEASE_COMMIT."
if [[ "$tag_commit" != "$release_sha" ]]; then
echo "Tag $VERSION points to $tag_commit, expected $release_sha."
exit 1
fi
echo "Verified tag $VERSION at $RELEASE_COMMIT."
echo "Verified tag $VERSION at $release_sha."
- name: Comment tag info to PR
uses: ./.github/actions/comment
@@ -261,5 +118,4 @@ 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 && steps.target.outputs.commit || steps.version.outputs.version }}
commit: ${{ steps.target.outputs.commit }}
ref: ${{ inputs.dry_run && github.sha || steps.version.outputs.version }}
+9 -22
View File
@@ -12,6 +12,9 @@ on:
dry_run:
required: true
type: boolean
secrets:
NPM_TOKEN:
required: false
permissions:
contents: read
@@ -36,7 +39,8 @@ jobs:
- name: Setup node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.x
node-version: 22.x
registry-url: https://registry.npmjs.org/
package-manager-cache: false
- name: Install dependencies
@@ -45,32 +49,15 @@ 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: npm publish "$PACKAGE_FILE" --dry-run --access public
run: pnpm publish -F artalk --no-git-checks --dry-run
- name: Publish
if: ${{ !inputs.dry_run }}
# npm validates build.yml, the calling workflow, for trusted publishing.
run: npm publish "$PACKAGE_FILE" --access public
run: pnpm publish -F artalk --no-git-checks --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# GitHub Packages
#
+23 -8
View File
@@ -1,5 +1,5 @@
name: Build and Publish
run-name: Build and Publish (${{ inputs.version || github.head_ref || github.ref }}) ${{ inputs.dry_run && '(🧪 Dry-Run)' || '' }}
run-name: Build and Publish (${{ github.head_ref || github.ref }}) ${{ inputs.dry_run && '(🧪 Dry-Run)' || '' }}
on:
pull_request:
@@ -15,22 +15,21 @@ 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"
type: choice
options:
- "ui,app,docker,rpm"
- "ui,app,docker"
- "ui,app"
- "ui,docker"
- "app,docker"
- "app,rpm"
- ui
- app
- docker
- rpm
default: "ui,app,docker"
permissions:
@@ -61,7 +60,6 @@ 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:
@@ -75,6 +73,8 @@ jobs:
permissions:
contents: read
id-token: write
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
# Build the Docker image
docker:
@@ -84,7 +84,6 @@ 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
@@ -104,9 +103,25 @@ jobs:
permissions:
contents: write
# Build RPM
rpm:
if: |
always() &&
needs.tagging.result == 'success' &&
!contains(fromJSON('["failure", "cancelled"]'), needs.app.result) &&
(inputs.build_items == '' || contains(format(',{0},', inputs.build_items), ',rpm,'))
needs: [tagging, app]
uses: ./.github/workflows/build-rpm.yml
permissions: {}
with:
version: ${{ needs.tagging.outputs.version }}
dry_run: ${{ inputs.dry_run || false }}
secrets:
COPR_BUILD_WEBHOOK_URL: ${{ secrets.COPR_BUILD_WEBHOOK_URL }}
# All builds are complete
done:
needs: [tagging, ui, docker, app]
needs: [tagging, ui, docker, app, rpm]
if: |
always() &&
needs.tagging.result == 'success' &&
-119
View File
@@ -45,7 +45,6 @@ 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*')"
@@ -54,31 +53,6 @@ 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
@@ -98,52 +72,6 @@ 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"
@@ -182,53 +110,6 @@ 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,14 +70,6 @@ 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,8 +40,6 @@ export default defineConfig(
'**/*.config.js',
'**/*.config.ts',
'**/*.d.ts',
'**/*.d.cts',
'**/*.d.mts',
],
},
-2
View File
@@ -19,8 +19,6 @@
"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",
+32 -50
View File
@@ -1,34 +1,15 @@
#!/usr/bin/env node
import { promises as fs, readFileSync } from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { promises as fs, readFileSync } from 'fs'
import path from 'path'
import { execFileSync } from 'child_process'
import process from 'process'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const REGISTRY_URL = 'https://registry.npmjs.org/'
const REQUEST_TIMEOUT = 10_000
const __dirname = path.dirname(new URL(import.meta.url).pathname)
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 || {})),
}
}
// Helper to run shell commands
const getPublishedVersion = (packageName) =>
execFileSync('pnpm', ['info', packageName, 'version'], { encoding: 'utf-8' }).trim()
// Recursively find all directories containing package.json
const findNodeProjects = async (dir) => {
@@ -55,8 +36,8 @@ const findNodeProjects = async (dir) => {
return projects
}
// Check whether the exact local version has already been published to npm.
const checkVersionPublished = async (projectPath) => {
// Compare local version with the latest npm version
const checkVersionMismatch = async (projectPath) => {
const packageJsonPath = path.join(projectPath, 'package.json')
try {
@@ -65,22 +46,23 @@ const checkVersionPublished = async (projectPath) => {
const localVersion = packageJson.version
const packageName = packageJson.name
const { latestVersion, versions } = await getPublishedPackage(packageName)
// Get the latest version from npm using pnpm info
const npmVersion = getPublishedVersion(packageName)
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) {
console.log(`✅ ${packageName} is up to date (${npmVersion})`)
} else {
console.log(`❌ ${packageName} is outdated (local: ${localVersion}, npm: ${npmVersion})`)
}
console.log(
`❌ ${packageName}@${localVersion} is not published (npm latest: ${latestVersion || 'none'})`,
)
return { packageName, localVersion, latestVersion }
if (localVersion !== npmVersion) {
return { packageName, localVersion, latestVersion: npmVersion }
}
} 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
@@ -97,8 +79,8 @@ const getArgs = () => {
return specifiedProject
}
// Main function to find unpublished local package versions
const findUnpublishedProjects = async () => {
// Main function to find outdated packages
const findOutdatedProjects = async () => {
const specifiedProject = getArgs()
let projects = await findNodeProjects(path.join(__dirname, '../ui'))
console.log(`Found ${projects.length} projects under 'ui' directory.\n`)
@@ -118,29 +100,29 @@ const findUnpublishedProjects = async () => {
}
}
const unpublishedProjects = []
const outdatedProjects = []
for (const project of projects) {
const result = await checkVersionPublished(project)
const result = await checkVersionMismatch(project)
if (result) {
unpublishedProjects.push(result)
outdatedProjects.push(result)
}
}
console.log('\n==================================================\n')
if (unpublishedProjects.length === 0) {
console.log('✅ Every local package version is published to npm.')
if (outdatedProjects.length === 0) {
console.log('✅ All projects have the latest versions pushed to npm.')
} else {
console.log('Local package versions not published to npm:\n')
unpublishedProjects.forEach(({ packageName, localVersion, latestVersion }) => {
console.log(`❌ ${packageName}: local ${localVersion}, npm latest ${latestVersion || 'none'}`)
console.log('Projects with outdated versions:\n')
outdatedProjects.forEach(({ packageName, localVersion, latestVersion }) => {
console.log(`❌ ${packageName}: Local version ${localVersion}, NPM version ${latestVersion}`)
})
process.exit(1)
}
}
findUnpublishedProjects().catch((error) => {
findOutdatedProjects().catch((error) => {
console.error(error)
process.exitCode = 1
})
+1 -8
View File
@@ -4,12 +4,6 @@
"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"
],
@@ -30,8 +24,7 @@
"scripts": {
"dev": "tsdown --watch",
"build": "tsdown",
"test": "vitest",
"test:attw": "pnpx @arethetypeswrong/cli@0.18.5 --pack ."
"test": "vitest"
},
"dependencies": {
"@typescript-eslint/utils": "^8.65.0"
+5 -22
View File
@@ -1,19 +1,8 @@
{
"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"
@@ -23,21 +12,15 @@
"types": "./dist/artalk-plugin-auth.d.ts",
"exports": {
".": {
"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"
}
"types": "./dist/artalk-plugin-auth.d.ts",
"import": "./dist/artalk-plugin-auth.mjs",
"require": "./dist/artalk-plugin-auth.cjs"
}
},
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview",
"test:attw": "pnpx @arethetypeswrong/cli@0.18.5 --pack ."
"serve": "vite preview"
},
"devDependencies": {
"@artalk/plugin-kit": "workspace:^",
@@ -47,6 +30,6 @@
"vite-plugin-solid": "^2.11.13"
},
"peerDependencies": {
"artalk": "^2.10.0"
"artalk": "workspace:^"
}
}
@@ -0,0 +1,3 @@
import type { ArtalkPlugin } from 'artalk'
declare const ArtalkAuthPlugin: ArtalkPlugin
+3
View File
@@ -0,0 +1,3 @@
import type { ArtalkPlugin } from 'artalk'
declare const ArtalkAuthPlugin: ArtalkPlugin
+5 -22
View File
@@ -1,19 +1,8 @@
{
"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",
@@ -24,8 +13,7 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview",
"test:attw": "pnpx @arethetypeswrong/cli@0.18.5 --pack ."
"serve": "vite preview"
},
"devDependencies": {
"@artalk/plugin-kit": "workspace:^",
@@ -34,19 +22,14 @@
"marked": "^18.0.7"
},
"peerDependencies": {
"artalk": ">=2.8.6 <3.0.0",
"artalk": "workspace:^",
"katex": ">=0.16.45 <0.18.0"
},
"exports": {
".": {
"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"
}
"types": "./dist/artalk-plugin-katex.d.ts",
"import": "./dist/artalk-plugin-katex.mjs",
"require": "./dist/artalk-plugin-katex.cjs"
}
}
}
-9
View File
@@ -1,9 +0,0 @@
import Artalk from 'artalk'
export {}
declare global {
interface Window {
Artalk?: typeof Artalk
}
}
+2 -1
View File
@@ -1,9 +1,10 @@
import Artalk from 'artalk'
import Artalk, { ArtalkPlugin } from 'artalk'
export {}
declare global {
interface Window {
Artalk?: typeof Artalk
ArtalkPlugins?: { [name: string]: ArtalkPlugin }
}
}
+3 -30
View File
@@ -2,16 +2,6 @@
"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",
@@ -26,32 +16,15 @@
"default": "./dist/main.mjs"
}
},
"./client": {
"import": {
"types": "./client.d.ts"
},
"require": {
"types": "./client.d.cts"
}
}
"./client": "./client.d.ts"
},
"scripts": {
"dev": "tsdown --watch",
"build": "tsdown",
"test:attw": "pnpx @arethetypeswrong/cli@0.18.5 --pack ."
"build": "tsdown"
},
"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"
@@ -62,7 +35,7 @@
"vite": "^8.1.5"
},
"peerDependencies": {
"artalk": "^2.10.0",
"artalk": "workspace:^",
"typescript": "*",
"vite": "*"
}
+1
View File
@@ -1,4 +1,5 @@
interface Window {
lightGallery: (el: any, options: any) => any | undefined
Fancybox?: any
lightbox?: any
PhotoSwipeLightbox?: any
+5 -22
View File
@@ -1,19 +1,8 @@
{
"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",
@@ -24,8 +13,7 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview",
"test:attw": "pnpx @arethetypeswrong/cli@0.18.5 --pack ."
"serve": "vite preview"
},
"devDependencies": {
"@artalk/plugin-kit": "workspace:^",
@@ -36,7 +24,7 @@
"photoswipe": "^5.4.4"
},
"peerDependencies": {
"artalk": "^2.9.1",
"artalk": "workspace:^",
"fancybox": "^3.0.1",
"lightbox2": "^2.12.0",
"lightgallery": "^2.9.0",
@@ -58,14 +46,9 @@
},
"exports": {
".": {
"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"
}
"types": "./dist/artalk-plugin-lightbox.d.ts",
"import": "./dist/artalk-plugin-lightbox.mjs",
"require": "./dist/artalk-plugin-lightbox.cjs"
}
}
}