From 983480131bf6ac48b3834d334deb97a6b3f2f4f6 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Mon, 4 Mar 2024 12:54:13 +0900
Subject: [PATCH 1/4] chore: Automated release (#13075)

* chore: Automated release

* follow
---
 .github/workflows/release-edit-with-push.yml |  40 ++++++
 .github/workflows/release-with-dispatch.yml  | 122 +++++++++++++++++++
 .github/workflows/release-with-ready.yml     |  38 ++++++
 3 files changed, 200 insertions(+)
 create mode 100644 .github/workflows/release-edit-with-push.yml
 create mode 100644 .github/workflows/release-with-dispatch.yml
 create mode 100644 .github/workflows/release-with-ready.yml

diff --git a/.github/workflows/release-edit-with-push.yml b/.github/workflows/release-edit-with-push.yml
new file mode 100644
index 0000000000..944b98eb7c
--- /dev/null
+++ b/.github/workflows/release-edit-with-push.yml
@@ -0,0 +1,40 @@
+name: "Release Manager: sync changelog with PR"
+
+on:
+  push:
+    branches:
+      - release/**
+    paths:
+      - 'CHANGELOG.md'
+
+env:
+  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+permissions:
+  contents: write
+  issues: write
+  pull-requests: write
+
+jobs:
+  edit:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      # headがrelease/かつopenのPRを1つ取得
+      - name: Get PR
+        run: |
+          echo "pr_number=$(gh pr list --limit 1 --head "${{ github.ref_name }}" --json number  --jq '.[] | .number')" >> $GITHUB_OUTPUT
+        id: get_pr
+      - name: Get target version
+        uses: misskey-dev/release-manager-actions/.github/actions/get-target-version@v1
+        id: v
+      # CHANGELOG.mdの内容を取得
+      - name: Get changelog
+        uses: misskey-dev/release-manager-actions/.github/actions/get-changelog@v1
+        with:
+          version: ${{ steps.v.outputs.target_version }}
+        id: changelog
+      # PRのnotesを更新
+      - name: Update PR
+        run: |
+          gh pr edit ${{ steps.get_pr.outputs.pr_number }} --body "${{ steps.changelog.outputs.changelog }}"
diff --git a/.github/workflows/release-with-dispatch.yml b/.github/workflows/release-with-dispatch.yml
new file mode 100644
index 0000000000..1a954739d9
--- /dev/null
+++ b/.github/workflows/release-with-dispatch.yml
@@ -0,0 +1,122 @@
+name: "Release Manager [Dispatch]"
+
+on:
+  workflow_dispatch:
+    inputs:
+      ## Specify the type of the next release.
+      #version_increment_type:
+      #  type: choice
+      #  description: 'VERSION INCREMENT TYPE'
+      #  default: 'patch'
+      #  required: false
+      #  options:
+      #    - 'major'
+      #    - 'minor'
+      #    - 'patch'
+      merge:
+        type: boolean
+        description: 'MERGE RELEASE BRANCH TO MAIN'
+        default: false
+
+env:
+  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+permissions:
+  contents: write
+  issues: write
+  pull-requests: write
+
+jobs:
+  get-pr:
+    runs-on: ubuntu-latest
+    outputs:
+      pr_number: ${{ steps.get_pr.outputs.pr_number }}
+    steps:
+      - uses: actions/checkout@v4
+      # headがrelease/かつopenのPRを1つ取得
+      - name: Get PRs
+        run: |
+          echo "pr_number=$(gh pr list --limit 1 --search "head:release/ is:open" --json number  --jq '.[] | .number')" >> $GITHUB_OUTPUT
+        id: get_pr
+
+  merge:
+    uses: misskey-dev/release-manager-actions/.github/workflows/merge.yml@v1
+    needs: get-pr
+    if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge == true }}
+    with:
+      pr_number: ${{ needs.get-pr.outputs.pr_number }}
+      package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
+      # Text to prepend to the changelog
+      # The first line must be `## Unreleased`
+      changes_template: |
+        ## Unreleased
+
+        ### General
+        -
+
+        ### Client
+        -
+
+        ### Server
+        -
+
+      use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
+    secrets:
+      RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
+      RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
+      RULESET_EDIT_APP_ID: ${{ secrets.RULESET_EDIT_APP_ID }}
+      RULESET_EDIT_APP_PRIVATE_KEY: ${{ secrets.RULESET_EDIT_APP_PRIVATE_KEY }}
+
+  create-prerelease:
+    uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v1
+    needs: get-pr
+    if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge != true  }}
+    with:
+      pr_number: ${{ needs.get-pr.outputs.pr_number }}
+      package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
+      use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
+    secrets:
+      RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
+      RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
+
+  create-target:
+    uses: misskey-dev/release-manager-actions/.github/workflows/create-target.yml@v1
+    needs: get-pr
+    if: ${{ needs.get-pr.outputs.pr_number == '' }}
+    with:
+      # The script for version increment.
+      # process.env.CURRENT_VERSION: The current version.
+      #
+      # Misskey calender versioning (yyyy.MM.patch) example
+      version_increment_script: |
+        const now = new Date();
+        const year = now.toLocaleDateString('en-US', { year: 'numeric', timeZone: 'Asia/Tokyo' });
+        const month = now.toLocaleDateString('en-US', { month: 'numeric', timeZone: 'Asia/Tokyo' });
+        const [major, minor, _patch] = process.env.CURRENT_VERSION.split('.');
+        const patch = Number(_patch.split('-')[0]);
+        if (Number.isNaN(patch)) {
+          console.error('Invalid patch version', year, month, process.env.CURRENT_VERSION, major, minor, _patch);
+          throw new Error('Invalid patch version');
+        }
+        if (year !== major || month !== minor) {
+          return `${year}.${month}.0`;
+        } else {
+          return `${major}.${minor}.${patch + 1}`;
+        }
+      ##Semver example
+      #version_increment_script: |
+      #  const [major, minor, patch] = process.env.CURRENT_VERSION.split('.');
+      #  if ("${{ inputs.version_increment_type }}" === "major") {
+      #    return `${Number(major) + 1}.0.0`;
+      #  } else if ("${{ inputs.version_increment_type }}" === "minor") {
+      #    return `${major}.${Number(minor) + 1}.0`;
+      #  } else {
+      #    return `${major}.${minor}.${Number(patch) + 1}`;
+      #  }
+      package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
+      use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
+    secrets:
+      RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
+      RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
+      RULESET_EDIT_APP_ID: ${{ secrets.RULESET_EDIT_APP_ID }}
+      RULESET_EDIT_APP_PRIVATE_KEY: ${{ secrets.RULESET_EDIT_APP_PRIVATE_KEY }}
diff --git a/.github/workflows/release-with-ready.yml b/.github/workflows/release-with-ready.yml
new file mode 100644
index 0000000000..b64ed20791
--- /dev/null
+++ b/.github/workflows/release-with-ready.yml
@@ -0,0 +1,38 @@
+name: "Release Manager: release RC when ready for review"
+
+on:
+  pull_request:
+    types: [ready_for_review]
+
+env:
+  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+permissions:
+  contents: write
+  issues: write
+  pull-requests: write
+
+jobs:
+  check:
+    runs-on: ubuntu-latest
+    outputs:
+      ref: ${{ steps.get_pr.outputs.ref }}
+    steps:
+      - uses: actions/checkout@v4
+      # PR情報を取得
+      - name: Get PR
+        run: |
+          pr_json=$(gh pr view ${{ github.event.pull_request.number }} --json isDraft,headRefName)
+          echo "ref=$(echo $pr_json | jq -r '.headRefName')" >> $GITHUB_OUTPUT
+        id: get_pr
+  release:
+    uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v1
+    needs: check
+    if: startsWith(needs.check.outputs.ref, 'release/')
+    with:
+      pr_number: ${{ github.event.pull_request.number }}
+      package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
+      use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
+    secrets:
+      RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
+      RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}

From 9542cb8d6253a93b06a68b9ac3647367f8f7354c Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Mon, 4 Mar 2024 13:48:57 +0900
Subject: [PATCH 2/4] =?UTF-8?q?fix(backend):=20=E3=83=AA=E3=83=A2=E3=83=BC?=
 =?UTF-8?q?=E3=83=88=E3=82=B5=E3=83=BC=E3=83=90=E3=83=BC=E3=81=AE=E6=83=85?=
 =?UTF-8?q?=E5=A0=B1=E3=81=8C=E6=9B=B4=E6=96=B0=E3=81=A7=E3=81=8D=E3=81=AA?=
 =?UTF-8?q?=E3=81=8F=E3=81=AA=E3=81=A3=E3=81=A6=E3=81=84=E3=81=9F=E5=95=8F?=
 =?UTF-8?q?=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3=20(#13507)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix(backend): fetchInstanceMetadataのLockが永遠に解除されない問題を修正

Co-authored-by: まっちゃとーにゅ <17376330+u1-liquid@users.noreply.github.com>

* fix test

* fix

* comment

* comment

* improve test

---------

Co-authored-by: まっちゃとーにゅ <17376330+u1-liquid@users.noreply.github.com>
---
 .../src/core/FetchInstanceMetadataService.ts  | 28 ++++++++++++++-----
 .../test/unit/FetchInstanceMetadataService.ts | 24 ++++++++++++++--
 2 files changed, 43 insertions(+), 9 deletions(-)

diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts
index bc270bd28f..8d173855f3 100644
--- a/packages/backend/src/core/FetchInstanceMetadataService.ts
+++ b/packages/backend/src/core/FetchInstanceMetadataService.ts
@@ -51,21 +51,35 @@ export class FetchInstanceMetadataService {
 	}
 
 	@bindThis
-	public async tryLock(host: string): Promise<boolean> {
-		const mutex = await this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '1', 'GET');
-		return mutex !== '1';
+	// public for test
+	public async tryLock(host: string): Promise<string | null> {
+		// TODO: マイグレーションなのであとで消す (2024.3.1)
+		this.redisClient.del(`fetchInstanceMetadata:mutex:${host}`);
+
+		return await this.redisClient.set(
+			`fetchInstanceMetadata:mutex:v2:${host}`, '1',
+			'EX', 30, // 30秒したら自動でロック解除 https://github.com/misskey-dev/misskey/issues/13506#issuecomment-1975375395
+			'GET' // 古い値を返す(なかったらnull)
+		);
 	}
 
 	@bindThis
-	public unlock(host: string): Promise<'OK'> {
-		return this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '0');
+	// public for test
+	public unlock(host: string): Promise<number> {
+		return this.redisClient.del(`fetchInstanceMetadata:mutex:v2:${host}`);
 	}
 
 	@bindThis
 	public async fetchInstanceMetadata(instance: MiInstance, force = false): Promise<void> {
 		const host = instance.host;
-		// Acquire mutex to ensure no parallel runs
-		if (!await this.tryLock(host)) return;
+
+		// finallyでunlockされてしまうのでtry内でロックチェックをしない
+		// (returnであってもfinallyは実行される)
+		if (!force && await this.tryLock(host) === '1') {
+			// 1が返ってきていたらロックされているという意味なので、何もしない
+			return;
+		}
+
 		try {
 			if (!force) {
 				const _instance = await this.federatedInstanceService.fetch(host);
diff --git a/packages/backend/test/unit/FetchInstanceMetadataService.ts b/packages/backend/test/unit/FetchInstanceMetadataService.ts
index 510b84b680..bf8f3ab0e3 100644
--- a/packages/backend/test/unit/FetchInstanceMetadataService.ts
+++ b/packages/backend/test/unit/FetchInstanceMetadataService.ts
@@ -56,6 +56,7 @@ describe('FetchInstanceMetadataService', () => {
 				} else if (token === DI.redis) {
 					return mockRedis;
 				}
+				return null;
 			})
 			.compile();
 
@@ -78,6 +79,7 @@ describe('FetchInstanceMetadataService', () => {
 		httpRequestService.getJson.mockImplementation(() => { throw Error(); });
 		const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
 		const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
+
 		await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
 		expect(tryLockSpy).toHaveBeenCalledTimes(1);
 		expect(unlockSpy).toHaveBeenCalledTimes(1);
@@ -92,6 +94,7 @@ describe('FetchInstanceMetadataService', () => {
 		httpRequestService.getJson.mockImplementation(() => { throw Error(); });
 		const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
 		const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
+
 		await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
 		expect(tryLockSpy).toHaveBeenCalledTimes(1);
 		expect(unlockSpy).toHaveBeenCalledTimes(1);
@@ -104,13 +107,30 @@ describe('FetchInstanceMetadataService', () => {
 		const now = Date.now();
 		federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
 		httpRequestService.getJson.mockImplementation(() => { throw Error(); });
+		await fetchInstanceMetadataService.tryLock('example.com');
 		const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
 		const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
-		await fetchInstanceMetadataService.tryLock('example.com');
+
 		await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
-		expect(tryLockSpy).toHaveBeenCalledTimes(2);
+		expect(tryLockSpy).toHaveBeenCalledTimes(1);
 		expect(unlockSpy).toHaveBeenCalledTimes(0);
 		expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0);
 		expect(httpRequestService.getJson).toHaveBeenCalledTimes(0);
 	});
+
+	test('Do when lock not acquired but forced', async () => {
+		redisClient.set = mockRedis();
+		const now = Date.now();
+		federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
+		httpRequestService.getJson.mockImplementation(() => { throw Error(); });
+		await fetchInstanceMetadataService.tryLock('example.com');
+		const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
+		const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
+
+		await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any, true);
+		expect(tryLockSpy).toHaveBeenCalledTimes(0);
+		expect(unlockSpy).toHaveBeenCalledTimes(1);
+		expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0);
+		expect(httpRequestService.getJson).toHaveBeenCalled();
+	});
 });

From 96ab1af03b821ac265a1e8ff492c154b80e02759 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Mon, 4 Mar 2024 16:09:24 +0900
Subject: [PATCH 3/4] Update CHANGELOG.md

---
 CHANGELOG.md | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index bafee277d2..ca7bf85fd0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,17 @@
 
 -->
 
+## Unreleased
+
+### General
+-
+
+### Client
+- 
+
+### Server
+-
+
 ## 2024.3.1
 
 ### General

From 13f5fafdbc869207141f2a2f1f75f61c3147372d Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Mon, 4 Mar 2024 10:39:43 +0000
Subject: [PATCH 4/4] remove template txt

---
 CHANGELOG.md | 14 --------------
 1 file changed, 14 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ca7bf85fd0..349e99d133 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,17 +1,3 @@
-<!--
-## 202x.x.x (unreleased)
-
-### General
--
-
-### Client
-- 
-
-### Server
--
-
--->
-
 ## Unreleased
 
 ### General