mirror of
https://github.com/github/codeql-action.git
synced 2026-10-03 09:14:58 +00:00
Merge remote-tracking branch 'origin/main' into mbg/improve-json-failures
This commit is contained in:
2
.github/workflows/__rubocop-multi-language.yml
generated
vendored
2
.github/workflows/__rubocop-multi-language.yml
generated
vendored
@@ -54,7 +54,7 @@ jobs:
|
||||
use-all-platform-bundle: 'false'
|
||||
setup-kotlin: 'true'
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
|
||||
uses: ruby/setup-ruby@984c0c890880bbf811283d6f09c4607c62d210a4 # v1.323.0
|
||||
with:
|
||||
ruby-version: 2.6
|
||||
- name: Install Code Scanning integration
|
||||
|
||||
@@ -4,7 +4,7 @@ See the [releases page](https://github.com/github/codeql-action/releases) for th
|
||||
|
||||
## [UNRELEASED]
|
||||
|
||||
No user facing changes.
|
||||
- Update default CodeQL bundle version to [2.27.1](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.27.1). [#4160](https://github.com/github/codeql-action/pull/4160)
|
||||
|
||||
## 4.38.1 - 18 Sept 2026
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"bundleVersion": "codeql-bundle-v2.27.0",
|
||||
"cliVersion": "2.27.0",
|
||||
"priorBundleVersion": "codeql-bundle-v2.26.4",
|
||||
"priorCliVersion": "2.26.4"
|
||||
"bundleVersion": "codeql-bundle-v2.27.1",
|
||||
"cliVersion": "2.27.1",
|
||||
"priorBundleVersion": "codeql-bundle-v2.27.0",
|
||||
"priorCliVersion": "2.27.0"
|
||||
}
|
||||
|
||||
8
lib/entry-points.js
generated
8
lib/entry-points.js
generated
@@ -144779,6 +144779,7 @@ function doubleQuoteWhitespaceOnly(layout) {
|
||||
function applyForceQuotesOption(layout) {
|
||||
if (!layout.presenterOptions.forceQuotes) return;
|
||||
if (layout.isKey || layout.style !== SCALAR_STYLE.PLAIN) return;
|
||||
if (layout.node.tag !== layout.presenterOptions.schema.defaultScalarTag.tagName) return;
|
||||
layout.style = layout.node.value.includes("\n") ? SCALAR_STYLE.DOUBLE_QUOTED : _preferredQuotedStyle(layout);
|
||||
}
|
||||
function tryLongOrMultilineAsBlock(layout) {
|
||||
@@ -147750,8 +147751,8 @@ var path6 = __toESM(require("path"));
|
||||
var semver4 = __toESM(require_semver2());
|
||||
|
||||
// src/defaults.json
|
||||
var bundleVersion = "codeql-bundle-v2.27.0";
|
||||
var cliVersion = "2.27.0";
|
||||
var bundleVersion = "codeql-bundle-v2.27.1";
|
||||
var cliVersion = "2.27.1";
|
||||
|
||||
// src/overlay/index.ts
|
||||
var fs5 = __toESM(require("fs"));
|
||||
@@ -153267,6 +153268,7 @@ async function getCodeQLForCmd(logger, cmd, checkVersion) {
|
||||
"--format=json",
|
||||
`--language=${language}`,
|
||||
"--extractor-include-aliases",
|
||||
"-J-XX:-UsePerfData",
|
||||
...getExtraOptionsFromEnv(["resolve", "extractor"])
|
||||
],
|
||||
{
|
||||
@@ -164517,7 +164519,7 @@ tmp/lib/tmp.js:
|
||||
*)
|
||||
|
||||
js-yaml/dist/js-yaml.mjs:
|
||||
(*! js-yaml 5.4.1 https://github.com/nodeca/js-yaml @license MIT *)
|
||||
(*! js-yaml 5.4.2 https://github.com/nodeca/js-yaml @license MIT *)
|
||||
|
||||
long/index.js:
|
||||
(**
|
||||
|
||||
32
package-lock.json
generated
32
package-lock.json
generated
@@ -31,7 +31,7 @@
|
||||
"follow-redirects": "^1.16.0",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"js-yaml": "^5.4.1",
|
||||
"js-yaml": "^5.4.2",
|
||||
"jsonschema": "1.5.0",
|
||||
"long": "^5.3.2",
|
||||
"node-forge": "^1.4.0",
|
||||
@@ -58,7 +58,7 @@
|
||||
"eslint-import-resolver-typescript": "^4.4.5",
|
||||
"eslint-plugin-github": "^6.1.2",
|
||||
"eslint-plugin-import-x": "^4.17.1",
|
||||
"eslint-plugin-jsdoc": "^64.3.8",
|
||||
"eslint-plugin-jsdoc": "^64.5.2",
|
||||
"eslint-plugin-no-async-foreach": "^0.1.1",
|
||||
"glob": "^13.0.6",
|
||||
"globals": "^17.12.0",
|
||||
@@ -5348,9 +5348,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-jsdoc": {
|
||||
"version": "64.3.8",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-64.3.8.tgz",
|
||||
"integrity": "sha512-JXLYE2BVfmbqLrrslb9/vg3URg8okW5pMnppM+3EYwvPCbuOiSXGOJWaNK208wDx6xXawkIFGtRYgFgrW23dsg==",
|
||||
"version": "64.5.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-64.5.2.tgz",
|
||||
"integrity": "sha512-GirLf/jpVQ/HSLVT98ztIrJ8GUlWWrrwExkofqzeIjjOxlqFtyqnpkRs30FLwEKh0xHEpgy35CU0qFn0I/Mh9w==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
@@ -5374,7 +5374,13 @@
|
||||
"node": "^22.22.2 || >=24.15.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0"
|
||||
"eslint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-jsdoc/node_modules/debug": {
|
||||
@@ -7091,9 +7097,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.1.tgz",
|
||||
"integrity": "sha512-28R/k+NAjeuf7+CKlTxWZVExJGwVVLwY06DgEnOMz2gEpfNkDcD7QvyiVPT0xy0XXhU8vHsd4Ot42OOPdJG7dQ==",
|
||||
"version": "5.4.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.2.tgz",
|
||||
"integrity": "sha512-m+aqu+LwO1O6sIopafj8HUVl5aawITwZQe/yHpMCKjaWBaA/d07B/QdMb3529REftiU+RMMHL3Vlsw3hON7vWg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -10367,9 +10373,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
|
||||
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz",
|
||||
"integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
@@ -10460,7 +10466,7 @@
|
||||
"lite-matter": "^0.1.2",
|
||||
"mdast-util-from-markdown": "^2.0.3",
|
||||
"semver": "^7.8.5",
|
||||
"yaml": "^2.9.0"
|
||||
"yaml": "^2.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.19.43",
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"follow-redirects": "^1.16.0",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"js-yaml": "^5.4.1",
|
||||
"js-yaml": "^5.4.2",
|
||||
"jsonschema": "1.5.0",
|
||||
"long": "^5.3.2",
|
||||
"node-forge": "^1.4.0",
|
||||
@@ -66,7 +66,7 @@
|
||||
"eslint-import-resolver-typescript": "^4.4.5",
|
||||
"eslint-plugin-github": "^6.1.2",
|
||||
"eslint-plugin-import-x": "^4.17.1",
|
||||
"eslint-plugin-jsdoc": "^64.3.8",
|
||||
"eslint-plugin-jsdoc": "^64.5.2",
|
||||
"eslint-plugin-no-async-foreach": "^0.1.1",
|
||||
"glob": "^13.0.6",
|
||||
"globals": "^17.12.0",
|
||||
|
||||
@@ -119,14 +119,3 @@ export function isValidChangenoteFile(filename: string): boolean {
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the change-note files of the given list of file paths, ignoring ".gitkeep".
|
||||
* @param filepaths A list of filepaths to validate
|
||||
* @returns True if all the paths are valid, false otherwise.
|
||||
*/
|
||||
export function isValidAllChangenoteFiles(filepaths: string[]): boolean {
|
||||
return filepaths
|
||||
.filter((f) => f !== ".gitkeep")
|
||||
.reduce((r, filePath) => r && isValidChangenoteFile(filePath), true);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import assert from "node:assert/strict";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import { withTmpDir, withTmpFile } from "../../src/util";
|
||||
import { withTmpFile } from "../../src/util";
|
||||
|
||||
import {
|
||||
hasValidChangenoteCategory,
|
||||
isValidAllChangenoteFiles,
|
||||
isValidChangenoteContent,
|
||||
isValidChangenoteFile,
|
||||
isValidChangenoteFilename,
|
||||
@@ -187,39 +184,3 @@ await describe("isValidChangenoteFile", async () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
await describe("isValidAllChangenoteFiles", async () => {
|
||||
await it("accepts list of file paths of valid change-notes", async () => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const fileName1 = path.join(tmpDir, "2026-01-01-fix-bug.md");
|
||||
const fileName2 = path.join(tmpDir, "2026-01-02-add-feature.md");
|
||||
fs.writeFileSync(fileName1, "---\ncategory: fix\n---\n- Fixed a bug\n");
|
||||
fs.writeFileSync(
|
||||
fileName2,
|
||||
"---\ncategory: feature\n---\n- Added a feature\n",
|
||||
);
|
||||
assert.equal(isValidAllChangenoteFiles([fileName1, fileName2]), true);
|
||||
});
|
||||
});
|
||||
|
||||
await it("accepts the empty list", async () => {
|
||||
assert.equal(isValidAllChangenoteFiles([]), true);
|
||||
});
|
||||
|
||||
await it("accepts list of .gitkeep", async () => {
|
||||
assert.equal(isValidAllChangenoteFiles([".gitkeep"]), true);
|
||||
});
|
||||
|
||||
await it("rejects list containing a file path to an invalid change-note", async () => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const fileName1 = path.join(tmpDir, "2026-01-01-fix-bug.md");
|
||||
const fileName2 = path.join(tmpDir, "2026-01-02-wrong-category.md");
|
||||
fs.writeFileSync(fileName1, "---\ncategory: fix\n---\n- Fixed a bug\n");
|
||||
fs.writeFileSync(
|
||||
fileName2,
|
||||
"---\ncategory: foobar\n---\n- Added a feature\n",
|
||||
);
|
||||
assert.equal(isValidAllChangenoteFiles([fileName1, fileName2]), false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
renderChangelog,
|
||||
withChangelog,
|
||||
} from "./changelog";
|
||||
import { isValidAllChangenoteFiles } from "./changelog/validate.mjs";
|
||||
import { isValidChangenoteFile } from "./changelog/validate.mjs";
|
||||
import { CHANGENOTES_DIR } from "./config";
|
||||
|
||||
/**
|
||||
@@ -116,7 +116,11 @@ function assemble(): ExitCode {
|
||||
|
||||
function validate(): ExitCode {
|
||||
try {
|
||||
if (isValidAllChangenoteFiles(fs.readdirSync(CHANGENOTES_DIR))) {
|
||||
const allChangenotesValid = getChangenotes().reduce(
|
||||
(r, changenote) => r && isValidChangenoteFile(changenote.absolutePath),
|
||||
true,
|
||||
);
|
||||
if (allChangenotesValid) {
|
||||
console.log(`All changenotes in '${CHANGENOTES_DIR}' are valid.`);
|
||||
return ExitCode.Success;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ versions:
|
||||
- default
|
||||
steps:
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
|
||||
uses: ruby/setup-ruby@984c0c890880bbf811283d6f09c4607c62d210a4 # v1.323.0
|
||||
with:
|
||||
ruby-version: 2.6
|
||||
- name: Install Code Scanning integration
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"lite-matter": "^0.1.2",
|
||||
"mdast-util-from-markdown": "^2.0.3",
|
||||
"semver": "^7.8.5",
|
||||
"yaml": "^2.9.0"
|
||||
"yaml": "^2.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.19.43",
|
||||
|
||||
@@ -937,6 +937,7 @@ async function getCodeQLForCmd(
|
||||
"--format=json",
|
||||
`--language=${language}`,
|
||||
"--extractor-include-aliases",
|
||||
"-J-XX:-UsePerfData",
|
||||
...getExtraOptionsFromEnv(["resolve", "extractor"]),
|
||||
],
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"bundleVersion": "codeql-bundle-v2.27.0",
|
||||
"cliVersion": "2.27.0",
|
||||
"priorBundleVersion": "codeql-bundle-v2.26.4",
|
||||
"priorCliVersion": "2.26.4"
|
||||
"bundleVersion": "codeql-bundle-v2.27.1",
|
||||
"cliVersion": "2.27.1",
|
||||
"priorBundleVersion": "codeql-bundle-v2.27.0",
|
||||
"priorCliVersion": "2.27.0"
|
||||
}
|
||||
|
||||
@@ -29,10 +29,13 @@ test.serial(
|
||||
process.env["GITHUB_SHA"] = currentSha;
|
||||
|
||||
const callback = sinon.stub(gitUtils, "getCommitOid");
|
||||
callback.withArgs("HEAD").resolves(currentSha);
|
||||
callback.withArgs(sinon.match.string, "HEAD").resolves(currentSha);
|
||||
|
||||
const actualRef = await gitUtils.getRef();
|
||||
t.deepEqual(actualRef, expectedRef);
|
||||
|
||||
t.is(callback.callCount, 1);
|
||||
t.true(callback.calledOnceWith(tmpDir, "HEAD"));
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -48,11 +51,17 @@ test.serial(
|
||||
const sha = "a".repeat(40);
|
||||
|
||||
const callback = sinon.stub(gitUtils, "getCommitOid");
|
||||
callback.withArgs("refs/remotes/pull/1/merge").resolves(sha);
|
||||
callback.withArgs("HEAD").resolves(sha);
|
||||
callback
|
||||
.withArgs(sinon.match.string, "refs/remotes/pull/1/merge")
|
||||
.resolves(sha);
|
||||
callback.withArgs(sinon.match.any, "HEAD").resolves(sha);
|
||||
|
||||
const actualRef = await gitUtils.getRef();
|
||||
t.deepEqual(actualRef, expectedRef);
|
||||
|
||||
t.is(callback.callCount, 2);
|
||||
t.true(callback.calledWith(tmpDir, "HEAD"));
|
||||
t.true(callback.calledWith(tmpDir, "refs/remotes/pull/1/merge"));
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -66,11 +75,18 @@ test.serial(
|
||||
process.env["GITHUB_SHA"] = "a".repeat(40);
|
||||
|
||||
const callback = sinon.stub(gitUtils, "getCommitOid");
|
||||
callback.withArgs(tmpDir, "refs/pull/1/merge").resolves("a".repeat(40));
|
||||
callback
|
||||
.withArgs(tmpDir, "refs/remotes/pull/1/merge")
|
||||
.resolves("a".repeat(40));
|
||||
callback.withArgs(tmpDir, "HEAD").resolves("b".repeat(40));
|
||||
callback.throws(new Error("Unexpected getCommitOid call in test."));
|
||||
|
||||
const actualRef = await gitUtils.getRef();
|
||||
t.deepEqual(actualRef, "refs/pull/1/head");
|
||||
|
||||
t.is(callback.callCount, 2);
|
||||
t.true(callback.calledWith(tmpDir, "refs/remotes/pull/1/merge"));
|
||||
t.true(callback.calledWith(tmpDir, "HEAD"));
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -92,11 +108,14 @@ test.serial(
|
||||
process.env["GITHUB_SHA"] = "a".repeat(40);
|
||||
|
||||
const callback = sinon.stub(gitUtils, "getCommitOid");
|
||||
callback.withArgs("refs/pull/1/merge").resolves("b".repeat(40));
|
||||
callback.withArgs("HEAD").resolves("b".repeat(40));
|
||||
callback.withArgs(tmpDir, "refs/pull/1/merge").resolves("b".repeat(40));
|
||||
callback.withArgs(sinon.match.any, "HEAD").resolves("b".repeat(40));
|
||||
|
||||
const actualRef = await gitUtils.getRef();
|
||||
t.deepEqual(actualRef, "refs/pull/2/merge");
|
||||
|
||||
// getCommitOid shouldn't be called, because the ref should be taken from the input
|
||||
t.is(callback.callCount, 0);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -38,7 +38,6 @@ async function checkEligibility(
|
||||
[ActionsEnvVars.RUNNER_ENVIRONMENT]: "github-hosted",
|
||||
}),
|
||||
features: createFeatures([Feature.PerLanguageBundles]),
|
||||
logger: getRecordingLogger([], { logToConsole: false }),
|
||||
...stateOverrides,
|
||||
}),
|
||||
{ ...ELIGIBLE_OPTIONS, ...overrides },
|
||||
@@ -134,7 +133,6 @@ test("getPerLanguageBundleLanguage explains a disabled feature before checking e
|
||||
const messages: LoggedMessage[] = [];
|
||||
const language = await getPerLanguageBundleLanguage(
|
||||
initAllState({
|
||||
env: getTestEnv(),
|
||||
features: createFeatures([]),
|
||||
logger: getRecordingLogger(messages, { logToConsole: false }),
|
||||
}),
|
||||
|
||||
@@ -102,8 +102,11 @@ export async function getPerLanguageBundleLanguage(
|
||||
return explain("the job is not running on a GitHub-hosted runner");
|
||||
}
|
||||
|
||||
// Check whether per-language bundles are published for the requested CLI version.
|
||||
// Latest-nightly selection skips this release-version check, but not the other eligibility checks.
|
||||
// Nightly releases are identified by dates rather than versions. If
|
||||
// `isLatestNightly` is `true`, the latest nightly is requested with
|
||||
// `tools: nightly` and we don't yet have the corresponding tag at this point.
|
||||
// Therefore, we skip the version check and don't have an equivalent.
|
||||
// We can safely assume that the latest nightly will have per-language bundles.
|
||||
if (!isLatestNightly) {
|
||||
if (cliVersion === undefined) {
|
||||
return explain("the requested CLI version is unknown");
|
||||
|
||||
@@ -69,25 +69,29 @@ function stubHostedNightly(tagName: string) {
|
||||
available: true,
|
||||
foundZstdBinary: true,
|
||||
});
|
||||
const fetchRelease = sinon
|
||||
.stub<Parameters<typeof fetch>, ReturnType<typeof fetch>>()
|
||||
.rejects(new Error("Unexpected API request in nightly bundle test"));
|
||||
fetchRelease
|
||||
.withArgs(
|
||||
"https://api.github.com/repos/dsp-testing/codeql-cli-nightlies/releases?per_page=1&page=1&prerelease=true",
|
||||
sinon.match({ method: "GET" }),
|
||||
)
|
||||
.callsFake(
|
||||
async () =>
|
||||
new Response(JSON.stringify([{ tag_name: tagName }]), {
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
);
|
||||
const client = github.getOctokit("123", {
|
||||
request: { fetch: fetchRelease },
|
||||
request: {
|
||||
fetch: async () => {
|
||||
throw new Error("Unexpected API request in nightly bundle test");
|
||||
},
|
||||
},
|
||||
});
|
||||
const listReleases = sinon
|
||||
.stub(client.rest.repos, "listReleases")
|
||||
.rejects(new Error("Unexpected release request in nightly bundle test"));
|
||||
listReleases
|
||||
.withArgs({
|
||||
owner: "dsp-testing",
|
||||
repo: "codeql-cli-nightlies",
|
||||
per_page: 1,
|
||||
page: 1,
|
||||
prerelease: true,
|
||||
})
|
||||
.resolves({
|
||||
data: [{ tag_name: tagName }],
|
||||
} as Awaited<ReturnType<typeof client.rest.repos.listReleases>>);
|
||||
sinon.stub(api, "getApiClient").value(() => client);
|
||||
return fetchRelease;
|
||||
return listReleases;
|
||||
}
|
||||
|
||||
test.serial("parse codeql bundle url version", (t) => {
|
||||
|
||||
@@ -682,7 +682,7 @@ export async function bundleDb(
|
||||
return databaseBundlePath;
|
||||
}
|
||||
|
||||
/** Returns the elapsed milliseconds, rounded, since a `performance.now()` timestamp. */
|
||||
/** Returns the elapsed milliseconds, rounded, since `startTime` was recorded with `performance.now()`. */
|
||||
export function durationMsSince(startTime: number): number {
|
||||
return Math.round(performance.now() - startTime);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user