Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7319e4733e | ||
|
|
4e86b8565b | ||
|
|
3a6baf0f12 | ||
|
|
e8c797e08e | ||
|
|
cf83be2c7f | ||
|
|
cfdd9b50bd | ||
|
|
cc92c9093e | ||
|
|
72f6bf584a | ||
|
|
f2899b4677 | ||
|
|
af306bddfe | ||
|
|
9927d3f5ec | ||
|
|
e5e4b800aa | ||
|
|
74f6bde645 | ||
|
|
0e6c75888a | ||
|
|
5cbce4f5ee | ||
|
|
df11ebfac2 | ||
|
|
50a5b0990c | ||
|
|
0e0bd99213 | ||
|
|
8250434419 | ||
|
|
9093186278 | ||
|
|
321f000b6d | ||
|
|
233ab9a35e | ||
|
|
d3a6c14a79 | ||
|
|
fa5d5d5a33 | ||
|
|
442eb645ce | ||
|
|
4d1e10f6d1 | ||
|
|
f63a22975a | ||
|
|
a9842f0f62 | ||
|
|
2728235f7d | ||
|
|
c2e0608dc4 | ||
|
|
bd74772a1a | ||
|
|
16e7903b2d | ||
|
|
f2c549b117 | ||
|
|
7a7d004438 | ||
|
|
9c4a92ec0d | ||
|
|
039214a996 | ||
|
|
2b373356cb | ||
|
|
1819382cf9 | ||
|
|
fb1eb39e74 | ||
|
|
7786b24bd8 |
@@ -24,7 +24,6 @@
|
|||||||
"@typescript-eslint/func-call-spacing": ["error", "never"],
|
"@typescript-eslint/func-call-spacing": ["error", "never"],
|
||||||
"@typescript-eslint/no-array-constructor": "error",
|
"@typescript-eslint/no-array-constructor": "error",
|
||||||
"@typescript-eslint/no-empty-interface": "error",
|
"@typescript-eslint/no-empty-interface": "error",
|
||||||
"@typescript-eslint/no-explicit-any": "error",
|
|
||||||
"@typescript-eslint/no-extraneous-class": "error",
|
"@typescript-eslint/no-extraneous-class": "error",
|
||||||
"@typescript-eslint/no-for-in-array": "error",
|
"@typescript-eslint/no-for-in-array": "error",
|
||||||
"@typescript-eslint/no-inferrable-types": "error",
|
"@typescript-eslint/no-inferrable-types": "error",
|
||||||
|
|||||||
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- run: |
|
- run: |
|
||||||
npm install
|
npm install
|
||||||
npm run all
|
npm run all
|
||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Make test pre-release
|
- name: Make test pre-release
|
||||||
uses: ./
|
uses: ./
|
||||||
with:
|
with:
|
||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
tag: ci-test-${{ matrix.os }}-${{ github.run_id }}
|
tag: ci-test-${{ matrix.os }}-${{ github.run_id }}
|
||||||
overwrite: true
|
overwrite: true
|
||||||
prerelease: true
|
prerelease: true
|
||||||
body: "rofl lol test"
|
body: "rofl lol test%0Aianal %25 fubar"
|
||||||
- name: Check that the uploaded asset is readable
|
- name: Check that the uploaded asset is readable
|
||||||
uses: actions/github-script@v2
|
uses: actions/github-script@v2
|
||||||
with:
|
with:
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
tag: "ci-test-${{ matrix.os }}-${{ github.run_id }}",
|
tag: "ci-test-${{ matrix.os }}-${{ github.run_id }}",
|
||||||
})
|
})
|
||||||
assert.deepStrictEqual(release.data.prerelease, true)
|
assert.deepStrictEqual(release.data.prerelease, true)
|
||||||
assert.deepStrictEqual(release.data.body, "rofl lol test")
|
assert.deepStrictEqual(release.data.body, "rofl lol test\nianal % fubar")
|
||||||
assert.deepStrictEqual(release.data.assets[0].name, "TEST.md")
|
assert.deepStrictEqual(release.data.assets[0].name, "TEST.md")
|
||||||
const actual = child_process.execSync(`curl -Ls ${release.data.assets[0].browser_download_url}`)
|
const actual = child_process.execSync(`curl -Ls ${release.data.assets[0].browser_download_url}`)
|
||||||
assert.deepStrictEqual(expected, actual)
|
assert.deepStrictEqual(expected, actual)
|
||||||
|
|||||||
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1,5 +1,15 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [2.5.0] - 2023-02-21
|
||||||
|
- Add retry to upload release [#96](https://github.com/svenstaro/upload-release-action/pull/96) (thanks @sonphantrung)
|
||||||
|
|
||||||
|
## [2.4.1] - 2023-02-01
|
||||||
|
- Modernize octokit usage
|
||||||
|
|
||||||
|
## [2.4.0] - 2023-01-09
|
||||||
|
- Update to node 16
|
||||||
|
- Bump most dependencies
|
||||||
|
|
||||||
## [2.3.0] - 2022-06-05
|
## [2.3.0] - 2022-06-05
|
||||||
- Now defaults `repo_token` to `${{ github.token }}` and `tag` to `${{ github.ref }}` [#69](https://github.com/svenstaro/upload-release-action/pull/69) (thanks @leighmcculloch)
|
- Now defaults `repo_token` to `${{ github.token }}` and `tag` to `${{ github.ref }}` [#69](https://github.com/svenstaro/upload-release-action/pull/69) (thanks @leighmcculloch)
|
||||||
|
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -1,4 +1,4 @@
|
|||||||
# Upload files to a GitHub release [](https://github.com/svenstaro/upload-release-action/actions)
|
# Upload files to a GitHub release [](https://github.com/svenstaro/upload-release-action/actions)
|
||||||
|
|
||||||
This action allows you to select which files to upload to the just-tagged release.
|
This action allows you to select which files to upload to the just-tagged release.
|
||||||
It runs on all operating systems types offered by GitHub.
|
It runs on all operating systems types offered by GitHub.
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --release
|
run: cargo build --release
|
||||||
- name: Upload binaries to release
|
- name: Upload binaries to release
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
asset_name: mything-macos-amd64
|
asset_name: mything-macos-amd64
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --release --locked
|
run: cargo build --release --locked
|
||||||
- name: Upload binaries to release
|
- name: Upload binaries to release
|
||||||
@@ -115,7 +115,7 @@ jobs:
|
|||||||
name: Publish binaries
|
name: Publish binaries
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --release
|
run: cargo build --release
|
||||||
- name: Upload binaries to release
|
- name: Upload binaries to release
|
||||||
@@ -144,7 +144,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --release
|
run: cargo build --release
|
||||||
- name: Upload binaries to release
|
- name: Upload binaries to release
|
||||||
@@ -182,7 +182,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
# This step reads a file from repo and use it for body of the release
|
# This step reads a file from repo and use it for body of the release
|
||||||
# This works on any self-hosted runner OS
|
# This works on any self-hosted runner OS
|
||||||
@@ -194,7 +194,7 @@ jobs:
|
|||||||
r="${r//'%'/'%25'}" # Multiline escape sequences for %
|
r="${r//'%'/'%25'}" # Multiline escape sequences for %
|
||||||
r="${r//$'\n'/'%0A'}" # Multiline escape sequences for '\n'
|
r="${r//$'\n'/'%0A'}" # Multiline escape sequences for '\n'
|
||||||
r="${r//$'\r'/'%0D'}" # Multiline escape sequences for '\r'
|
r="${r//$'\r'/'%0D'}" # Multiline escape sequences for '\r'
|
||||||
echo "::set-output name=RELEASE_BODY::$r" # <--- Set environment variable
|
echo "RELEASE_BODY=$r" >> $GITHUB_OUTPUT # <--- Set environment variable
|
||||||
|
|
||||||
- name: Upload Binaries to Release
|
- name: Upload Binaries to Release
|
||||||
uses: svenstaro/upload-release-action@v2
|
uses: svenstaro/upload-release-action@v2
|
||||||
|
|||||||
@@ -34,5 +34,5 @@ outputs:
|
|||||||
browser_download_url:
|
browser_download_url:
|
||||||
description: 'The publicly available URL of the asset.'
|
description: 'The publicly available URL of the asset.'
|
||||||
runs:
|
runs:
|
||||||
using: 'node12'
|
using: 'node16'
|
||||||
main: 'dist/index.js'
|
main: 'dist/index.js'
|
||||||
|
|||||||
20594
dist/index.js
vendored
20594
dist/index.js
vendored
File diff suppressed because one or more lines are too long
13297
package-lock.json
generated
13297
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
37
package.json
37
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "upload-release-action",
|
"name": "upload-release-action",
|
||||||
"version": "2.3.0",
|
"version": "2.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Upload files to a GitHub release",
|
"description": "Upload files to a GitHub release",
|
||||||
"main": "lib/main.js",
|
"main": "lib/main.js",
|
||||||
@@ -27,24 +27,25 @@
|
|||||||
"author": "Sven-Hendrik Haase",
|
"author": "Sven-Hendrik Haase",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.2.6",
|
"@actions/core": "^1.10.0",
|
||||||
"@actions/github": "^4.0.0",
|
"@actions/github": "^5",
|
||||||
"@types/glob": "^7.1.2",
|
"@lifeomic/attempt": "^3.0.3",
|
||||||
"glob": "^7.1.6"
|
"glob": "^7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^24.9.1",
|
"@types/glob": "^7",
|
||||||
"@types/node": "^12.12.64",
|
"@types/jest": "^29",
|
||||||
"@typescript-eslint/parser": "^3.5.0",
|
"@types/node": "^16",
|
||||||
"@zeit/ncc": "^0.22.3",
|
"@typescript-eslint/parser": "^5",
|
||||||
"eslint": "^7.10.0",
|
"@vercel/ncc": "^0.36.0",
|
||||||
"eslint-plugin-github": "^4.0.1",
|
"eslint": "^8",
|
||||||
"eslint-plugin-jest": "^23.17.1",
|
"eslint-plugin-github": "^4.6",
|
||||||
"jest": "^26.5.2",
|
"eslint-plugin-jest": "^27",
|
||||||
"jest-circus": "^26.5.2",
|
"jest": "^29",
|
||||||
"js-yaml": "^3.14.0",
|
"jest-circus": "^29",
|
||||||
"prettier": "^2.0.5",
|
"js-yaml": "^4",
|
||||||
"ts-jest": "^26.4.1",
|
"prettier": "^2.8",
|
||||||
"typescript": "^3.9.6"
|
"ts-jest": "^29",
|
||||||
|
"typescript": "^4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
88
src/main.ts
88
src/main.ts
@@ -5,11 +5,21 @@ import * as core from '@actions/core'
|
|||||||
import * as github from '@actions/github'
|
import * as github from '@actions/github'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as glob from 'glob'
|
import * as glob from 'glob'
|
||||||
|
import {retry} from '@lifeomic/attempt'
|
||||||
|
|
||||||
type RepoAssetsResp = Endpoints['GET /repos/:owner/:repo/releases/:release_id/assets']['response']['data']
|
const releaseByTag = 'GET /repos/{owner}/{repo}/releases/tags/{tag}' as const
|
||||||
type ReleaseByTagResp = Endpoints['GET /repos/:owner/:repo/releases/tags/:tag']['response']
|
const createRelease = 'POST /repos/{owner}/{repo}/releases' as const
|
||||||
type CreateReleaseResp = Endpoints['POST /repos/:owner/:repo/releases']['response']
|
const repoAssets =
|
||||||
type UploadAssetResp = Endpoints['POST /repos/:owner/:repo/releases/:release_id/assets{?name,label}']['response']
|
'GET /repos/{owner}/{repo}/releases/{release_id}/assets' as const
|
||||||
|
const uploadAssets =
|
||||||
|
'POST {origin}/repos/{owner}/{repo}/releases/{release_id}/assets{?name,label}' as const
|
||||||
|
const deleteAssets =
|
||||||
|
'DELETE /repos/{owner}/{repo}/releases/assets/{asset_id}' as const
|
||||||
|
|
||||||
|
type ReleaseByTagResp = Endpoints[typeof releaseByTag]['response']
|
||||||
|
type CreateReleaseResp = Endpoints[typeof createRelease]['response']
|
||||||
|
type RepoAssetsResp = Endpoints[typeof repoAssets]['response']['data']
|
||||||
|
type UploadAssetResp = Endpoints[typeof uploadAssets]['response']
|
||||||
|
|
||||||
async function get_release_by_tag(
|
async function get_release_by_tag(
|
||||||
tag: string,
|
tag: string,
|
||||||
@@ -20,17 +30,17 @@ async function get_release_by_tag(
|
|||||||
): Promise<ReleaseByTagResp | CreateReleaseResp> {
|
): Promise<ReleaseByTagResp | CreateReleaseResp> {
|
||||||
try {
|
try {
|
||||||
core.debug(`Getting release by tag ${tag}.`)
|
core.debug(`Getting release by tag ${tag}.`)
|
||||||
return await octokit.repos.getReleaseByTag({
|
return await octokit.request(releaseByTag, {
|
||||||
...repo(),
|
...repo(),
|
||||||
tag: tag
|
tag: tag
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
// If this returns 404, we need to create the release first.
|
// If this returns 404, we need to create the release first.
|
||||||
if (error.status === 404) {
|
if (error.status === 404) {
|
||||||
core.debug(
|
core.debug(
|
||||||
`Release for tag ${tag} doesn't exist yet so we'll create it now.`
|
`Release for tag ${tag} doesn't exist yet so we'll create it now.`
|
||||||
)
|
)
|
||||||
return await octokit.repos.createRelease({
|
return await octokit.request(createRelease, {
|
||||||
...repo(),
|
...repo(),
|
||||||
tag_name: tag,
|
tag_name: tag,
|
||||||
prerelease: prerelease,
|
prerelease: prerelease,
|
||||||
@@ -49,7 +59,7 @@ async function upload_to_release(
|
|||||||
asset_name: string,
|
asset_name: string,
|
||||||
tag: string,
|
tag: string,
|
||||||
overwrite: boolean,
|
overwrite: boolean,
|
||||||
octokit: Octokit
|
octokit: ReturnType<(typeof github)['getOctokit']>
|
||||||
): Promise<undefined | string> {
|
): Promise<undefined | string> {
|
||||||
const stat = fs.statSync(file)
|
const stat = fs.statSync(file)
|
||||||
if (!stat.isFile()) {
|
if (!stat.isFile()) {
|
||||||
@@ -57,23 +67,20 @@ async function upload_to_release(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const file_size = stat.size
|
const file_size = stat.size
|
||||||
const file_bytes = fs.readFileSync(file)
|
const file_bytes: any = fs.createReadStream(file)
|
||||||
|
|
||||||
// Check for duplicates.
|
// Check for duplicates.
|
||||||
const assets: RepoAssetsResp = await octokit.paginate(
|
const assets: RepoAssetsResp = await octokit.paginate(repoAssets, {
|
||||||
octokit.repos.listReleaseAssets,
|
...repo(),
|
||||||
{
|
release_id: release.data.id
|
||||||
...repo(),
|
})
|
||||||
release_id: release.data.id
|
|
||||||
}
|
|
||||||
)
|
|
||||||
const duplicate_asset = assets.find(a => a.name === asset_name)
|
const duplicate_asset = assets.find(a => a.name === asset_name)
|
||||||
if (duplicate_asset !== undefined) {
|
if (duplicate_asset !== undefined) {
|
||||||
if (overwrite) {
|
if (overwrite) {
|
||||||
core.debug(
|
core.debug(
|
||||||
`An asset called ${asset_name} already exists in release ${tag} so we'll overwrite it.`
|
`An asset called ${asset_name} already exists in release ${tag} so we'll overwrite it.`
|
||||||
)
|
)
|
||||||
await octokit.repos.deleteReleaseAsset({
|
await octokit.request(deleteAssets, {
|
||||||
...repo(),
|
...repo(),
|
||||||
asset_id: duplicate_asset.id
|
asset_id: duplicate_asset.id
|
||||||
})
|
})
|
||||||
@@ -88,15 +95,22 @@ async function upload_to_release(
|
|||||||
}
|
}
|
||||||
|
|
||||||
core.debug(`Uploading ${file} to ${asset_name} in release ${tag}.`)
|
core.debug(`Uploading ${file} to ${asset_name} in release ${tag}.`)
|
||||||
const uploaded_asset: UploadAssetResp = await octokit.repos.uploadReleaseAsset(
|
const uploaded_asset: UploadAssetResp = await retry(
|
||||||
|
async () => {
|
||||||
|
return octokit.request(uploadAssets, {
|
||||||
|
...repo(),
|
||||||
|
release_id: release.data.id,
|
||||||
|
url: release.data.upload_url,
|
||||||
|
name: asset_name,
|
||||||
|
data: file_bytes,
|
||||||
|
headers: {
|
||||||
|
'content-type': 'binary/octet-stream',
|
||||||
|
'content-length': file_size
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
{
|
{
|
||||||
url: release.data.upload_url,
|
maxAttempts: 3
|
||||||
name: asset_name,
|
|
||||||
data: file_bytes,
|
|
||||||
headers: {
|
|
||||||
'content-type': 'binary/octet-stream',
|
|
||||||
'content-length': file_size
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return uploaded_asset.data.browser_download_url
|
return uploaded_asset.data.browser_download_url
|
||||||
@@ -108,17 +122,17 @@ function repo(): {owner: string; repo: string} {
|
|||||||
if (!repo_name) {
|
if (!repo_name) {
|
||||||
return github.context.repo
|
return github.context.repo
|
||||||
}
|
}
|
||||||
const owner = repo_name.substr(0, repo_name.indexOf('/'))
|
const owner = repo_name.substring(0, repo_name.indexOf('/'))
|
||||||
if (!owner) {
|
if (!owner) {
|
||||||
throw new Error(`Could not extract 'owner' from 'repo_name': ${repo_name}.`)
|
throw new Error(`Could not extract 'owner' from 'repo_name': ${repo_name}.`)
|
||||||
}
|
}
|
||||||
const repo = repo_name.substr(repo_name.indexOf('/') + 1)
|
const repo_ = repo_name.substring(repo_name.indexOf('/') + 1)
|
||||||
if (!repo) {
|
if (!repo_) {
|
||||||
throw new Error(`Could not extract 'repo' from 'repo_name': ${repo_name}.`)
|
throw new Error(`Could not extract 'repo' from 'repo_name': ${repo_name}.`)
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
owner,
|
owner,
|
||||||
repo
|
repo: repo_
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,9 +150,13 @@ async function run(): Promise<void> {
|
|||||||
const overwrite = core.getInput('overwrite') == 'true' ? true : false
|
const overwrite = core.getInput('overwrite') == 'true' ? true : false
|
||||||
const prerelease = core.getInput('prerelease') == 'true' ? true : false
|
const prerelease = core.getInput('prerelease') == 'true' ? true : false
|
||||||
const release_name = core.getInput('release_name')
|
const release_name = core.getInput('release_name')
|
||||||
const body = core.getInput('body')
|
const body = core
|
||||||
|
.getInput('body')
|
||||||
|
.replace(/%0A/gi, '\n')
|
||||||
|
.replace(/%0D/gi, '\r')
|
||||||
|
.replace(/%25/g, '%')
|
||||||
|
|
||||||
const octokit: Octokit = github.getOctokit(token)
|
const octokit = github.getOctokit(token)
|
||||||
const release = await get_release_by_tag(
|
const release = await get_release_by_tag(
|
||||||
tag,
|
tag,
|
||||||
prerelease,
|
prerelease,
|
||||||
@@ -150,11 +168,11 @@ async function run(): Promise<void> {
|
|||||||
if (file_glob) {
|
if (file_glob) {
|
||||||
const files = glob.sync(file)
|
const files = glob.sync(file)
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
for (const file of files) {
|
for (const file_ of files) {
|
||||||
const asset_name = path.basename(file)
|
const asset_name = path.basename(file_)
|
||||||
const asset_download_url = await upload_to_release(
|
const asset_download_url = await upload_to_release(
|
||||||
release,
|
release,
|
||||||
file,
|
file_,
|
||||||
asset_name,
|
asset_name,
|
||||||
tag,
|
tag,
|
||||||
overwrite,
|
overwrite,
|
||||||
@@ -180,7 +198,7 @@ async function run(): Promise<void> {
|
|||||||
)
|
)
|
||||||
core.setOutput('browser_download_url', asset_download_url)
|
core.setOutput('browser_download_url', asset_download_url)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
core.setFailed(error.message)
|
core.setFailed(error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user