Repository: expressjs/serve-static

Issue:
Remove AppVeyor Configuration and Integrate Windows Testing in GitHub Actions
The repository currently includes an AppVeyor configuration file (`appveyor.yml`), which appears to be used for testing on Windows. However, GitHub Actions is already in use for CI/CD, and Windows-based testing can be integrated into the existing GitHub Actions workflow. Is AppVeyor providing any unique value that GitHub Actions does not?

## Proposed Changes  
- Integrate Windows testing into the GitHub Actions workflow.  
- Remove the `appveyor.yml` configuration if it's no longer needed.  
- Update the README to remove the AppVeyor status badge.  


Hints from issue comments:
@bjohansebas: Yep, we should remove AppVeyor and run the tests on Windows. That's something being done in the other repositories that still have AppVeyor@ctcpip: @jonchurch 👀 @jonchurch: We can remove appveyor, I dont think our appveyor integration is functional anymore

We've removed it in express main repo, as an example. You can see there how we setup windows testing in the matrix. 

Solution patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b902866..9030ed5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -4,78 +4,95 @@ on:
   push:
     branches:
       - master
-      - '2.0'
     paths-ignore:
       - '*.md'
   pull_request:
     paths-ignore:
       - '*.md'
 
+permissions:
+  contents: read
+
+# Cancel in progress workflows
+# in the scenario where we already had a run going for that PR/branch/tag but then triggered a new run
+concurrency:
+  group: "${{ github.workflow }} ✨ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}"
+  cancel-in-progress: true
+
 jobs:
-  test:
+  lint:
+    name: Lint
     runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Setup Node.js
+        uses: actions/setup-node@v4
+        with:
+          node-version: "lts/*"
+
+      - name: Install dependencies
+        run: npm install --ignore-scripts --include=dev
+
+      - name: Run lint
+        run: npm run lint
+
+  test:
+    name: Test - Node.js ${{ matrix.node-version }} - ${{ matrix.os }}
+    runs-on: ${{ matrix.os }}
     strategy:
+      fail-fast: false
       matrix:
-        name:
-        - Node.js 18.x
-        - Node.js 20.x
-        - Node.js 22.x
+        os: [ubuntu-latest, windows-latest]
+        # Node.js release schedule: https://nodejs.org/en/about/releases/
+        node-version: [18, 19, 20, 21, 22, 23]
+    steps:
+      - uses: actions/checkout@v4
 
-        include:
-        - name: Node.js 18.x
-          node-version: "18"
+      - name: Setup Node.js ${{ matrix.node-version }}
+        uses: actions/setup-node@v4
+        with:
+          check-latest: true
+          node-version: ${{ matrix.node-version }}
 
-        - name: Node.js 20.x
-          node-version: "20"
+      - name: Configure npm loglevel
+        run: npm config set loglevel error
 
-        - name: Node.js 22.x
-          node-version: "22"
+      - name: Install dependencies
+        run: npm install
 
-    steps:
-    - uses: actions/checkout@v4
-
-    - name: Install Node.js ${{ matrix.node-version }}
-      shell: bash -eo pipefail -l {0}
-      run: |
-        nvm install --default ${{ matrix.node-version }}
-        dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH"
-
-    - name: Configure npm
-      run: npm config set package-lock false
-
-    - name: Install Node.js dependencies
-      run: npm install
-
-    - name: List environment
-      id: list_env
-      shell: bash
-      run: |
-        echo "node@$(node -v)"
-        echo "npm@$(npm -v)"
-        npm -s ls ||:
-        (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print "::set-output name=" $2 "::" $3 }'
-
-    - name: Run tests
-      shell: bash
-      run: npm run test-ci
-
-    - name: Lint code
-      if: steps.list_env.outputs.eslint != ''
-      run: npm run lint
-
-    - name: Collect code coverage
-      uses: coverallsapp/github-action@master
-      with:
-        github-token: ${{ secrets.GITHUB_TOKEN }}
-        flag-name: run-${{ matrix.test_number }}
-        parallel: true
+      - name: Run tests
+        run: npm run test-ci
+
+      - name: Upload code coverage
+        uses: actions/upload-artifact@v4
+        with:
+          name: coverage-node-${{ matrix.node-version }}-${{ matrix.os }}
+          path: ./coverage/lcov.info
+          retention-days: 1
 
   coverage:
     needs: test
     runs-on: ubuntu-latest
+    permissions:
+      contents: read
+      checks: write
     steps:
-    - name: Upload code coverage
-      uses: coverallsapp/github-action@master
-      with:
-        github-token: ${{ secrets.github_token }}
-        parallel-finished: true
+      - uses: actions/checkout@v4
+
+      - name: Install lcov
+        run: sudo apt-get -y install lcov
+
+      - name: Collect coverage reports
+        uses: actions/download-artifact@v4
+        with:
+          path: ./coverage
+          pattern: coverage-node-*
+
+      - name: Merge coverage reports
+        run: find ./coverage -name lcov.info -exec printf '-a %q\n' {} \; | xargs lcov -o ./lcov.info
+
+      - name: Upload coverage report
+        uses: coverallsapp/github-action@v2
+        with:
+          file: ./lcov.info
diff --git a/HISTORY.md b/HISTORY.md
index 4e519ab..33164ff 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -27,6 +27,7 @@
   * Change `dotfiles` option default to `'ignore'`
   * Drop support for Node.js 0.8
   * Remove `hidden` option; use `dotfiles` option instead
+  * Remove `mime` export; use `mime-types` package instead
   * deps: send@1.0.0-beta.1
     - Use `mime-types` for file to content type mapping
     - deps: debug@3.1.0
diff --git a/README.md b/README.md
index 56268f7..70f01c3 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,7 @@
 
 [![NPM Version][npm-version-image]][npm-url]
 [![NPM Downloads][npm-downloads-image]][npm-url]
-[![Linux Build][github-actions-ci-image]][github-actions-ci-url]
-[![Windows Build][appveyor-image]][appveyor-url]
+[![CI][github-actions-ci-image]][github-actions-ci-url]
 [![Test Coverage][coveralls-image]][coveralls-url]
 
 ## Install
@@ -243,8 +242,6 @@ function setCustomCacheControl (res, file) {
 
 [MIT](LICENSE)
 
-[appveyor-image]: https://badgen.net/appveyor/ci/dougwilson/serve-static/master?label=windows
-[appveyor-url]: https://ci.appveyor.com/project/dougwilson/serve-static
 [coveralls-image]: https://badgen.net/coveralls/c/github/expressjs/serve-static/master
 [coveralls-url]: https://coveralls.io/r/expressjs/serve-static?branch=master
 [github-actions-ci-image]: https://badgen.net/github/checks/expressjs/serve-static/master?label=linux
diff --git a/SECURITY.md b/SECURITY.md
deleted file mode 100644
index 3817eaf..0000000
--- a/SECURITY.md
+++ /dev/null
@@ -1,50 +0,0 @@
-# Security Policies and Procedures
-
-This document outlines security procedures and general policies for the Express
-project.
-
-  * [Reporting a Bug](#reporting-a-bug)
-  * [Disclosure Policy](#disclosure-policy)
-  * [Comments on this Policy](#comments-on-this-policy)
-
-## Reporting a Bug
-
-The Express team and community take all security bugs in Express seriously.
-Thank you for improving the security of Express. We appreciate your efforts and
-responsible disclosure and will make every effort to acknowledge your
-contributions.
-
-Report security bugs by emailing the lead maintainer in the Readme.md file.
-
-To ensure the timely response to your report, please ensure that the entirety
-of the report is contained within the email body and not solely behind a web
-link or an attachment.
-
-The lead maintainer will acknowledge your email within 48 hours, and will send a
-more detailed response within 48 hours indicating the next steps in handling
-your report. After the initial reply to your report, the security team will
-endeavor to keep you informed of the progress towards a fix and full
-announcement, and may ask for additional information or guidance.
-
-Report security bugs in third-party modules to the person or team maintaining
-the module.
-
-## Disclosure Policy
-
-When the security team receives a security bug report, they will assign it to a
-primary handler. This person will coordinate the fix and release process,
-involving the following steps:
-
-  * Confirm the problem and determine the affected versions.
-  * Audit code to find any potential similar problems.
-  * Prepare fixes for all releases still under maintenance. These fixes will be
-    released as fast as possible to npm.
-
-## The Express Threat Model
-
-We are currently working on a new version of the security model, the most updated version can be found [here](https://github.com/expressjs/security-wg/blob/main/docs/ThreatModel.md)
-
-## Comments on this Policy
-
-If you have suggestions on how this process could be improved please submit a
-pull request.
diff --git a/appveyor.yml b/appveyor.yml
deleted file mode 100644
index fa48bc3..0000000
--- a/appveyor.yml
+++ /dev/null
@@ -1,88 +0,0 @@
-environment:
-  matrix:
-    - nodejs_version: "0.10"
-    - nodejs_version: "0.12"
-    - nodejs_version: "1.8"
-    - nodejs_version: "2.5"
-    - nodejs_version: "3.3"
-    - nodejs_version: "4.9"
-    - nodejs_version: "5.12"
-    - nodejs_version: "6.17"
-    - nodejs_version: "7.10"
-    - nodejs_version: "8.16"
-    - nodejs_version: "9.11"
-    - nodejs_version: "10.24"
-    - nodejs_version: "11.15"
-    - nodejs_version: "12.22"
-    - nodejs_version: "13.14"
-    - nodejs_version: "14.19"
-    - nodejs_version: "15.14"
-    - nodejs_version: "16.14"
-    - nodejs_version: "17.8"
-cache:
-  - node_modules
-install:
-  # Install Node.js
-  - ps: >-
-      try { Install-Product node $env:nodejs_version -ErrorAction Stop }
-      catch { Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version) }
-  # Configure npm
-  - ps: |
-      # Skip updating shrinkwrap / lock
-      npm config set shrinkwrap false
-  # Remove all non-test dependencies
-  - ps: |
-      # Remove coverage dependency
-      npm rm --silent --save-dev nyc
-      # Remove lint dependencies
-      cmd.exe /c "node -pe `"Object.keys(require('./package').devDependencies).join('\n')`"" | `
-        sls "^eslint(-|$)" | `
-        %{ npm rm --silent --save-dev $_ }
-  # Setup Node.js version-specific dependencies
-  - ps: |
-      # mocha for testing
-      # - use 3.x for Node.js < 4
-      # - use 5.x for Node.js < 6
-      # - use 6.x for Node.js < 8
-      # - use 7.x for Node.js < 10
-      # - use 8.x for Node.js < 12
-      if ([int]$env:nodejs_version.split(".")[0] -lt 4) {
-        npm install --silent --save-dev mocha@3.5.3
-      } elseif ([int]$env:nodejs_version.split(".")[0] -lt 6) {
-        npm install --silent --save-dev mocha@5.2.0
-      } elseif ([int]$env:nodejs_version.split(".")[0] -lt 8) {
-        npm install --silent --save-dev mocha@6.2.3
-      } elseif ([int]$env:nodejs_version.split(".")[0] -lt 10) {
-        npm install --silent --save-dev mocha@7.2.0
-      } elseif ([int]$env:nodejs_version.split(".")[0] -lt 12) {
-        npm install --silent --save-dev mocha@8.4.0
-      }
-  - ps: |
-      # supertest for http calls
-      # - use 2.0.0 for Node.js < 4
-      # - use 3.4.2 for Node.js < 6
-      if ([int]$env:nodejs_version.split(".")[0] -lt 4) {
-        npm install --silent --save-dev supertest@2.0.0
-      } elseif ([int]$env:nodejs_version.split(".")[0] -lt 6) {
-        npm install --silent --save-dev supertest@3.4.2
-      } elseif ([int]$env:nodejs_version.split(".")[0] -lt 8) {
-        npm install --silent --save-dev supertest@6.1.6
-      }
-  # Update Node.js modules
-  - ps: |
-      # Prune & rebuild node_modules
-      if (Test-Path -Path node_modules) {
-        npm prune
-        npm rebuild
-      }
-  # Install Node.js modules
-  - npm install
-build: off
-test_script:
-  # Output version data
-  - ps: |
-      node --version
-      npm --version
-  # Run test script
-  - npm test
-version: "{build}"
diff --git a/index.js b/index.js
index 1bee463..a7d0083 100644
--- a/index.js
+++ b/index.js
@@ -198,11 +198,22 @@ function createRedirectDirectoryListener () {
 
     // send redirect response
     res.statusCode = 301
-    res.setHeader('Content-Type', 'text/html; charset=UTF-8')
-    res.setHeader('Content-Length', Buffer.byteLength(doc))
-    res.setHeader('Content-Security-Policy', "default-src 'none'")
-    res.setHeader('X-Content-Type-Options', 'nosniff')
-    res.setHeader('Location', loc)
+    setHeaderIfNotSet(res, 'Content-Type', 'text/html; charset=UTF-8')
+    setHeaderIfNotSet(res, 'Content-Length', Buffer.byteLength(doc))
+    setHeaderIfNotSet(res, 'Content-Security-Policy', "default-src 'none'")
+    setHeaderIfNotSet(res, 'X-Content-Type-Options', 'nosniff')
+    setHeaderIfNotSet(res, 'Location', loc)
     res.end(doc)
   }
 }
+
+/**
+ * Set default value for the header only if it is not already set in the response
+ * @private
+ */
+
+function setHeaderIfNotSet (res, name, value) {
+  if (!res.hasHeader(name)) {
+    res.setHeader(name, value)
+  }
+}
diff --git a/package.json b/package.json
index b70f167..a0b23d5 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,6 @@
     "eslint-plugin-standard": "4.1.0",
     "mocha": "^10.7.0",
     "nyc": "^17.0.0",
-    "safe-buffer": "^5.2.1",
     "supertest": "^6.3.4"
   },
   "files": [



Test patch:
diff --git a/test/test.js b/test/test.js
index 6984bcd..c92ce00 100644
--- a/test/test.js
+++ b/test/test.js
@@ -1,6 +1,5 @@
 
 var assert = require('assert')
-var Buffer = require('safe-buffer').Buffer
 var http = require('http')
 var path = require('path')
 var request = require('supertest')
@@ -469,6 +468,9 @@ describe('serveStatic()', function () {
     before(function () {
       server = createServer(fixtures, null, function (req, res) {
         req.url = req.url.replace(/\/snow(\/|$)/, '/snow \u2603$1')
+        if (req.url.match(/\/pets/)) {
+          res.setHeader('Content-Security-Policy', "default-src 'self'")
+        }
       })
     })
 
@@ -508,13 +510,20 @@ describe('serveStatic()', function () {
         .expect(301, />Redirecting to \/snow%20%E2%98%83\/</, done)
     })
 
-    it('should respond with default Content-Security-Policy', function (done) {
+    it('should respond with default Content-Security-Policy when header is not set', function (done) {
       request(server)
         .get('/users')
         .expect('Content-Security-Policy', "default-src 'none'")
         .expect(301, done)
     })
 
+    it('should respond with custom Content-Security-Policy when header is set', function (done) {
+      request(server)
+        .get('/pets')
+        .expect('Content-Security-Policy', "default-src 'self'")
+        .expect(301, done)
+    })
+
     it('should not redirect incorrectly', function (done) {
       request(server)
         .get('/')