mirror of
https://github.com/hanzoai/pack.git
synced 2026-09-30 23:43:13 +00:00
feat(node): gracefully handle out of sync package-lock.json (#643)
* feat(node): add out-of-sync package-lock npm example - Add new example demonstrating npm ci failure with an out-of-sync lockfile - Include test configuration and documentation for the scenario Generated-by: aiautocommit * fix(node): fallback to npm install when lockfile is missing - Always use `npm install` for npm projects to prevent build failures when `package-lock.json` is missing. - Log a warning when the lockfile is absent to encourage generating one. - Clean up outdated comments regarding package manifest parsing. Generated-by: aiautocommit * test(core): update test snapshots for npm install changes Generated-by: aiautocommit * feat(node): add npm ci recommendation and railpack example - Document recommendations for preferring `npm ci` over `npm install` for Node.js projects using npm. - Add a corresponding `railpack.json` example demonstrating how to configure deterministic installs with `npm ci`. Generated-by: aiautocommit * refactor(core): use npm ci in build plan for node npm examples - Switch from `npm install` to `npm ci` for deterministic builds when package-lock.json is present. - Remove redundant cache directory creation step. Generated-by: aiautocommit * feat(node): add suggestions for missing lockfiles - Log suggestions when `package-lock.json` or `pnpm-lock.yaml` is missing to encourage deterministic installs. - Update architecture documentation with guidance on committing `package-lock.json`. Generated-by: aiautocommit * test(core): update out-of-sync package-lock snapshot for builder changes Generated-by: aiautocommit * feat(node): allow overriding npm install command via config variable Generated-by: aiautocommit * docs(node): document RAILPACK_NODE_NPM_INSTALL environment variable Generated-by: aiautocommit * docs: recommend committing all package manager lockfiles Generated-by: aiautocommit
This commit is contained in:
@@ -105,7 +105,7 @@
|
||||
"src": "package.json"
|
||||
},
|
||||
{
|
||||
"cmd": "npm ci"
|
||||
"cmd": "npm install"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
"src": "package.json"
|
||||
},
|
||||
{
|
||||
"cmd": "npm ci"
|
||||
"cmd": "npm install"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
"src": "package.json"
|
||||
},
|
||||
{
|
||||
"cmd": "npm ci"
|
||||
"cmd": "npm install"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
|
||||
+1
-1
@@ -125,7 +125,7 @@
|
||||
"src": "package.json"
|
||||
},
|
||||
{
|
||||
"cmd": "npm ci"
|
||||
"cmd": "npm install"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
|
||||
+1
-1
@@ -129,7 +129,7 @@
|
||||
"src": "package.json"
|
||||
},
|
||||
{
|
||||
"cmd": "npm ci"
|
||||
"cmd": "npm install"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
"src": "package.json"
|
||||
},
|
||||
{
|
||||
"cmd": "npm ci"
|
||||
"cmd": "npm install"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
"src": "package.json"
|
||||
},
|
||||
{
|
||||
"cmd": "npm ci"
|
||||
"cmd": "npm install"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
|
||||
+1
-1
@@ -114,7 +114,7 @@
|
||||
"src": "package.json"
|
||||
},
|
||||
{
|
||||
"cmd": "npm ci"
|
||||
"cmd": "npm install"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
"src": "packages/web/package.json"
|
||||
},
|
||||
{
|
||||
"cmd": "npm ci"
|
||||
"cmd": "npm install"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
|
||||
@@ -102,9 +102,6 @@
|
||||
{
|
||||
"path": "/app/node_modules/.bin"
|
||||
},
|
||||
{
|
||||
"cmd": "mkdir -p /app/node_modules/.cache"
|
||||
},
|
||||
{
|
||||
"dest": "package-lock.json",
|
||||
"src": "package-lock.json"
|
||||
@@ -114,7 +111,8 @@
|
||||
"src": "package.json"
|
||||
},
|
||||
{
|
||||
"cmd": "npm ci"
|
||||
"cmd": "sh -c 'npm ci'",
|
||||
"customName": "npm ci"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"cmd": "mkdir -p /app/node_modules/.cache"
|
||||
},
|
||||
{
|
||||
"cmd": "npm ci"
|
||||
"cmd": "npm install"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
"src": "prisma"
|
||||
},
|
||||
{
|
||||
"cmd": "npm ci"
|
||||
"cmd": "npm install"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
"src": "package.json"
|
||||
},
|
||||
{
|
||||
"cmd": "npm ci"
|
||||
"cmd": "npm install"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"src": "package.json"
|
||||
},
|
||||
{
|
||||
"cmd": "npm ci"
|
||||
"cmd": "npm install"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
"src": "package.json"
|
||||
},
|
||||
{
|
||||
"cmd": "npm ci"
|
||||
"cmd": "npm install"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
"src": "packages/utils/package.json"
|
||||
},
|
||||
{
|
||||
"cmd": "npm ci"
|
||||
"cmd": "npm install"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
"src": "package.json"
|
||||
},
|
||||
{
|
||||
"cmd": "npm ci"
|
||||
"cmd": "npm install"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
|
||||
Executable
+174
@@ -0,0 +1,174 @@
|
||||
{
|
||||
"caches": {
|
||||
"apt": {
|
||||
"directory": "/var/cache/apt",
|
||||
"type": "locked"
|
||||
},
|
||||
"apt-lists": {
|
||||
"directory": "/var/lib/apt/lists",
|
||||
"type": "locked"
|
||||
},
|
||||
"node-modules": {
|
||||
"directory": "/app/node_modules/.cache",
|
||||
"type": "shared"
|
||||
},
|
||||
"npm-install": {
|
||||
"directory": "/root/.npm",
|
||||
"type": "shared"
|
||||
}
|
||||
},
|
||||
"deploy": {
|
||||
"base": {
|
||||
"step": "packages:apt:runtime"
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"include": [
|
||||
"/mise/shims",
|
||||
"/mise/installs",
|
||||
"/usr/local/bin/mise",
|
||||
"/etc/mise/config.toml",
|
||||
"/root/.local/state/mise"
|
||||
],
|
||||
"step": "packages:mise"
|
||||
},
|
||||
{
|
||||
"include": [
|
||||
"/app/node_modules"
|
||||
],
|
||||
"step": "build"
|
||||
},
|
||||
{
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
".yarn"
|
||||
],
|
||||
"include": [
|
||||
"/root/.cache",
|
||||
"."
|
||||
],
|
||||
"step": "build"
|
||||
}
|
||||
],
|
||||
"startCommand": "npm run start",
|
||||
"variables": {
|
||||
"CI": "true",
|
||||
"NODE_ENV": "production",
|
||||
"NPM_CONFIG_FETCH_RETRIES": "5",
|
||||
"NPM_CONFIG_FUND": "false",
|
||||
"NPM_CONFIG_PRODUCTION": "false",
|
||||
"NPM_CONFIG_UPDATE_NOTIFIER": "false",
|
||||
"RAILPACK_VERSION": "dev"
|
||||
}
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"assets": {
|
||||
"generated-mise-toml": "[generated-mise-toml]"
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"path": "/mise/shims"
|
||||
},
|
||||
{
|
||||
"customName": "create mise config",
|
||||
"name": "generated-mise-toml",
|
||||
"path": "/etc/mise/config.toml"
|
||||
},
|
||||
{
|
||||
"cmd": "mise install",
|
||||
"customName": "install mise packages: node"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"image": "ghcr.io/railwayapp/railpack-builder:mise-2026.7.11"
|
||||
}
|
||||
],
|
||||
"name": "packages:mise",
|
||||
"variables": {
|
||||
"MISE_CACHE_DIR": "/mise/cache",
|
||||
"MISE_CONFIG_DIR": "/mise",
|
||||
"MISE_DATA_DIR": "/mise",
|
||||
"MISE_INSTALLS_DIR": "/mise/installs",
|
||||
"MISE_SHIMS_DIR": "/mise/shims"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caches": [
|
||||
"npm-install"
|
||||
],
|
||||
"commands": [
|
||||
{
|
||||
"path": "/app/node_modules/.bin"
|
||||
},
|
||||
{
|
||||
"cmd": "mkdir -p /app/node_modules/.cache"
|
||||
},
|
||||
{
|
||||
"dest": "package-lock.json",
|
||||
"src": "package-lock.json"
|
||||
},
|
||||
{
|
||||
"dest": "package.json",
|
||||
"src": "package.json"
|
||||
},
|
||||
{
|
||||
"cmd": "npm install"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"step": "packages:mise"
|
||||
}
|
||||
],
|
||||
"name": "install",
|
||||
"variables": {
|
||||
"CI": "true",
|
||||
"NODE_ENV": "production",
|
||||
"NPM_CONFIG_FETCH_RETRIES": "5",
|
||||
"NPM_CONFIG_FUND": "false",
|
||||
"NPM_CONFIG_PRODUCTION": "false",
|
||||
"NPM_CONFIG_UPDATE_NOTIFIER": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caches": [
|
||||
"node-modules"
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"step": "install"
|
||||
},
|
||||
{
|
||||
"include": [
|
||||
"."
|
||||
],
|
||||
"local": true
|
||||
}
|
||||
],
|
||||
"name": "build",
|
||||
"secrets": [
|
||||
"*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"caches": [
|
||||
"apt",
|
||||
"apt-lists"
|
||||
],
|
||||
"commands": [
|
||||
{
|
||||
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y libatomic1'",
|
||||
"customName": "install apt packages: libatomic1"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"image": "ghcr.io/railwayapp/railpack-runtime:mise-2026.7.11"
|
||||
}
|
||||
],
|
||||
"name": "packages:apt:runtime"
|
||||
}
|
||||
]
|
||||
}
|
||||
+1
-1
@@ -228,7 +228,7 @@
|
||||
"src": "package.json"
|
||||
},
|
||||
{
|
||||
"cmd": "npm ci"
|
||||
"cmd": "npm install"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
|
||||
+1
-1
@@ -228,7 +228,7 @@
|
||||
"src": "package.json"
|
||||
},
|
||||
{
|
||||
"cmd": "npm ci"
|
||||
"cmd": "npm install"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
|
||||
@@ -181,7 +181,7 @@
|
||||
"src": "package.json"
|
||||
},
|
||||
{
|
||||
"cmd": "npm ci"
|
||||
"cmd": "npm install"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
|
||||
@@ -33,8 +33,6 @@ var (
|
||||
// PackageManifestFiles is the precedence order Railpack searches for a Node
|
||||
// package manifest. package.json wins if both are present; package.json5 is
|
||||
// supported because pnpm reads it natively (https://pnpm.io/package_json).
|
||||
// Railpack's App.ReadJSON pipes through hujson, which accepts comments and
|
||||
// trailing commas — the common subset of JSON5 used in real manifests.
|
||||
PackageManifestFiles = []string{"package.json", "package.json5"}
|
||||
)
|
||||
|
||||
|
||||
@@ -128,12 +128,17 @@ func (p PackageManager) installDeps(ctx *generate.GenerateContext, install *gene
|
||||
|
||||
switch p {
|
||||
case PackageManagerNpm:
|
||||
hasLockfile := ctx.App.HasFile("package-lock.json")
|
||||
if hasLockfile {
|
||||
install.AddCommand(plan.NewExecCommand("npm ci"))
|
||||
} else {
|
||||
install.AddCommand(plan.NewExecCommand("npm install"))
|
||||
if !ctx.App.HasFile("package-lock.json") {
|
||||
ctx.Logger.LogSuggestion("Add a `package-lock.json` for more deterministic installs", "/architecture/recommendations")
|
||||
}
|
||||
|
||||
// ideally, `npm ci` should be used instead of `npm install`, but we default to npm install to avoid build failures
|
||||
// https://github.com/railwayapp/railpack/pull/643
|
||||
installCmd := "npm install"
|
||||
if customInstallCmd, _ := ctx.Env.GetConfigVariable("NODE_NPM_INSTALL"); customInstallCmd != "" {
|
||||
installCmd = customInstallCmd
|
||||
}
|
||||
install.AddCommand(plan.NewExecCommand(installCmd))
|
||||
case PackageManagerPnpm:
|
||||
install.AddEnvVars(map[string]string{
|
||||
"PNPM_HOME": PNPM_HOME,
|
||||
@@ -162,10 +167,10 @@ func (p PackageManager) installDeps(ctx *generate.GenerateContext, install *gene
|
||||
install.AddCommand(plan.NewExecCommand("pnpm add -g node-gyp"))
|
||||
}
|
||||
|
||||
hasLockfile := ctx.App.HasFile("pnpm-lock.yaml")
|
||||
if hasLockfile {
|
||||
if ctx.App.HasFile("pnpm-lock.yaml") {
|
||||
install.AddCommand(plan.NewExecCommand("pnpm install --frozen-lockfile --prefer-offline"))
|
||||
} else {
|
||||
ctx.Logger.LogSuggestion("Add a `pnpm-lock.yaml` for more deterministic installs", "/architecture/recommendations")
|
||||
install.AddCommand(plan.NewExecCommand("pnpm install"))
|
||||
}
|
||||
case PackageManagerBun:
|
||||
|
||||
@@ -5,11 +5,26 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/railwayapp/railpack/core/plan"
|
||||
"github.com/railwayapp/railpack/core/resolver"
|
||||
testingUtils "github.com/railwayapp/railpack/core/testing"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestInstallDeps_NpmInstallOverride(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "package.json"), []byte("{}"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "package-lock.json"), []byte("{}"), 0o644))
|
||||
|
||||
ctx := testingUtils.CreateGenerateContext(t, tmpDir)
|
||||
ctx.Env.Variables["RAILPACK_NODE_NPM_INSTALL"] = "npm ci"
|
||||
install := ctx.NewCommandStep("install")
|
||||
|
||||
PackageManagerNpm.installDeps(ctx, install, false)
|
||||
|
||||
require.Contains(t, install.Commands, plan.NewExecCommand("npm ci"))
|
||||
}
|
||||
|
||||
func TestGetPackageManagerPackages_PnpmLockfileVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -115,3 +115,63 @@ locked = true
|
||||
|
||||
Commit the generated `mise.lock` file alongside your `mise.toml`. Railpack
|
||||
automatically includes `mise.lock` files in the build when present.
|
||||
|
||||
## Commit Package Manager Lockfiles
|
||||
|
||||
Always generate and commit a lockfile from your package manager
|
||||
(`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, `Cargo.lock`,
|
||||
`poetry.lock`, etc.), and keep it in sync with your dependency manifests.
|
||||
|
||||
Without a lockfile (or with one that has drifted) installs resolve versions
|
||||
at build time. That leads to non-deterministic images and therefore possible
|
||||
bugs that don't occur locally.
|
||||
|
||||
## Prefer `npm ci` for npm Node Projects
|
||||
|
||||
When using npm as your package manager, set `RAILPACK_NODE_NPM_INSTALL` to
|
||||
opt into `npm ci` instead of the default `npm install`:
|
||||
|
||||
```bash
|
||||
RAILPACK_NODE_NPM_INSTALL="npm ci"
|
||||
```
|
||||
|
||||
You can also customize the install command in `railpack.json` if you need more
|
||||
control over the install step:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://schema.railpack.com",
|
||||
"steps": {
|
||||
"install": {
|
||||
// Configuring a step auto-adds it to deploy with include: ["."]. Use an empty
|
||||
// deployOutputs so install is not copied into the final image; node_modules
|
||||
// still reach the image via the build step.
|
||||
"deployOutputs": [],
|
||||
"commands": [
|
||||
// auto-generated commands from railpack
|
||||
{ "path": "/app/node_modules/.bin" },
|
||||
{ "src": "package-lock.json", "dest": "package-lock.json" },
|
||||
{ "src": "package.json", "dest": "package.json" },
|
||||
// by default, `npm install` is used here; use `npm ci` for increased determinism
|
||||
"npm ci"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Railpack defaults to `npm install` because `package.json` and
|
||||
`package-lock.json` often drift out of sync — usually from npm bugs rather than
|
||||
intentional local changes. That mismatch rarely shows up in development and only
|
||||
fails at image build time with:
|
||||
|
||||
> `npm ci` can only install packages when your package.json and package-lock.json
|
||||
> or npm-shrinkwrap.json are in sync.
|
||||
|
||||
Using `npm install` avoids build failures at the cost of weaker
|
||||
determinism. You should keep your lockfile in sync and opt into `npm ci` for:
|
||||
|
||||
- **Deterministic installs** — exact versions from the lockfile, matching local
|
||||
and CI environments, with no silent drift in production
|
||||
- **Faster installs** — `npm ci` installs from the lockfile instead of resolving
|
||||
`package.json` again
|
||||
|
||||
@@ -92,6 +92,7 @@ Railpack determines the start command in the following order:
|
||||
| `RAILPACK_NO_SPA` | Disable SPA mode | `true` |
|
||||
| `RAILPACK_SPA_OUTPUT_DIR` | Directory containing built static files | `dist` |
|
||||
| `RAILPACK_PRUNE_DEPS` | Remove development dependencies | `true` |
|
||||
| `RAILPACK_NODE_NPM_INSTALL` | Custom npm install command | `npm ci` |
|
||||
| `RAILPACK_NODE_PRUNE_CMD` | Custom command to prune dependencies | `npm prune --omit=dev --ignore-scripts` |
|
||||
| `RAILPACK_NODE_INSTALL_PATTERNS` | Custom patterns to install dependencies | `prisma` |
|
||||
| `RAILPACK_ANGULAR_PROJECT` | Name of the Angular project to build | `my-app` |
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "https://schema.railpack.com",
|
||||
"steps": {
|
||||
"install": {
|
||||
// Railpack defaults to `npm install` so out-of-sync lockfiles still succeed.
|
||||
// Opt into `npm ci` when the lockfile is kept in sync for deterministic installs.
|
||||
//
|
||||
// Configuring a step auto-adds it to deploy with include: ["."]. Use an empty
|
||||
// deployOutputs so install is not copied into the final image; node_modules
|
||||
// still reach the image via the build step.
|
||||
"deployOutputs": [],
|
||||
"commands": [
|
||||
{ "path": "/app/node_modules/.bin" },
|
||||
{ "src": "package-lock.json", "dest": "package-lock.json" },
|
||||
{ "src": "package.json", "dest": "package.json" },
|
||||
"npm ci"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Out-of-sync npm lockfile
|
||||
|
||||
This example intentionally keeps `package-lock.json` out of sync with
|
||||
`package.json` to reproduce an `npm ci` failure. The lockfile was generated
|
||||
with only `dayjs` installed; `is-number` was then added to `package.json`
|
||||
without regenerating the lockfile.
|
||||
|
||||
Do not run `npm install` in this directory unless you are recreating the
|
||||
out-of-sync fixture.
|
||||
@@ -0,0 +1,4 @@
|
||||
import dayjs from "dayjs";
|
||||
import isNumber from "is-number";
|
||||
|
||||
console.log(`npm dependencies loaded: ${dayjs("2026-07-21").format("YYYY-MM-DD")} ${isNumber(42)}`);
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "npm-out-of-sync-package-lock",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "npm-out-of-sync-package-lock",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"dayjs": "1.11.13"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.13",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "npm-out-of-sync-package-lock",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"dayjs": "1.11.13",
|
||||
"is-number": "7.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{
|
||||
"expectedOutput": "npm dependencies loaded: 2026-07-21 true",
|
||||
// NPM_CONFIG_PRODUCTION currently emits a deprecation warning during installation.
|
||||
"stderrAllowed": true
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user