Full Code of ai/ssdeploy for AI

main cd65ade8bc5f cached
32 files
38.1 KB
11.1k tokens
37 symbols
1 requests
Download .txt
Repository: ai/ssdeploy
Branch: main
Commit: cd65ade8bc5f
Files: 32
Total size: 38.1 KB

Directory structure:
gitextract_gk28koth/

├── .editorconfig
├── .github/
│   └── workflows/
│       └── test.yml
├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── bin.js
├── configs/
│   ├── .dockerignore
│   ├── Dockerfile
│   ├── close.yml
│   ├── deploy.yml
│   ├── nginx-template.conf
│   └── preview.yml
├── lib/
│   ├── build.js
│   ├── call-cloudflare.js
│   ├── changed.js
│   ├── debug.js
│   ├── deploy.js
│   ├── detect-docker.js
│   ├── dirs.js
│   ├── find-package-up.js
│   ├── init.js
│   ├── purge.js
│   ├── run-image.js
│   ├── show-error.js
│   ├── show-help.js
│   ├── show-spinner.js
│   ├── show-version.js
│   └── sign.js
├── package.json
└── purge.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true


================================================
FILE: .github/workflows/test.yml
================================================
name: Test
on:
  push:
    branches:
      - main
  pull_request:
permissions:
  contents: read
jobs:
  test:
    name: Test
    runs-on: ubuntu-latest
    steps:
      - name: Checkout the repository
        uses: actions/checkout@v3
      - name: Install pnpm
        uses: pnpm/action-setup@v2
        with:
          version: 7.29.3
      - name: Install Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: pnpm
      - name: Install all dependencies
        run: pnpm install --frozen-lockfile
      - name: Run tests
        run: pnpm test


================================================
FILE: .gitignore
================================================
node_modules/


================================================
FILE: .npmignore
================================================
pnpm-lock.yaml

example.png


================================================
FILE: CHANGELOG.md
================================================
# Change Log
This project adheres to [Semantic Versioning](http://semver.org/).

## 0.9.3
* Move to the new Github Actions output API.

## 0.9.2
* Fixed sending all traffic to latest revision.

## 0.9.1
* Fixed `run` command for server with custom `Dockerfile` and without `dist`.

## 0.9
* Added `preview` command.
* Added `close` preview.
* Added GitHub Deployment to CI.
* Added workflows for preview pull requests.

## 0.8.1
* Updated `dotenv`.
* Updated GitHub Actions config example.

## 0.8
* Added `pnpm` support.

## 0.7.2
* Replaced `nanocolors` with `piococolors`.
* Reduced package size.

## 0.7.1
* Replaced `colorette` with `nanocolors`.

## 0.7
* Dropped Node.js 13 support.
* Reduced dependencies.

## 0.6.21
* Speed-up Docker build.

## 0.6.20
* Updated workflow config.
* Removed `ora`.

## 0.6.19
* Update deploy workflow example.

## 0.6.18
* Fix `sign` command.

## 0.6.17
* Added color output to CI on deploy.

## 0.6.16
* Fixed `.avif` support.

## 0.6.15
* Fixed `colorette` imports.

## 0.6.14
* Replace color output library.

## 0.6.13
* Reduce dependencies.

## 0.6.12
* Fix Node.js 14 support.

## 0.6.11
* Improve image downloading output.

## 0.6.10
* Use `stderr` for debug messages.

## 0.6.9
* Remove color from GitHub Actions output variable.

## 0.6.8
* Fix GitHub Actions output variable.

## 0.6.7
* Fix mark to check `ssdeploy changed` hash.

## 0.6.6
* Use configs in `ssdeploy changed` hash.
* Remove white spaces from `WEBSITE_URL` environment variable too.
* Fix GitHub Actions output variable.
* Fix text in `ssdeploy changed`.

## 0.6.5
* Fix error message.

## 0.6.4
* Fix loading previous hash in `ssdeploy changed`.

## 0.6.3
* Fix syntax error in GitHub Actions workflow.

## 0.6.2
* Clean up GitHub Actions workflow.

## 0.6.1
* Fix GitHub Actions workflow for `ssdeploy changed`.

## 0.6
* Do not spend resources to deploy website if files were not changed.
* Fix colors on GitHub Actions.

## 0.5.1
* Fix `trim()` error on default region environment variable.

## 0.5
* Add `npm` support.
* Add `--verbose` by default to deploy workflow.
* Fix error on new line and spaces in environment variables.

## 0.4.2
* Fix deploy without previous images to clean.

## 0.4.1
* Show step number during the build.

## 0.4
* Rename `node_modules/ssdeploy/purge` to `node_modules/ssdeploy/purge.js`.
* Move configs to `node_modules/ssdeploy/configs`.

## 0.3.1
* Fix cleaned images count.

## 0.3
* Show the size of the image after build.

## 0.2.4
* Fix first deploy.
* Fix deploy on local laptop.

## 0.2.3
* Clean up GitHub Actions workflow.

## 0.2.2
* Add `--verbose` support to `ssdeploy run` and `ssdeploy shell`.

## 0.2.1
* Do not open browser on `ssdeploy shell`.

## 0.2
* Rename project from `solid-state-deploy` to `ssdeploy`.
* Add `shell` command.

## 0.1
* Initial release.


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright 2019 Andrey Sitnik <andrey@sitnik.ru>

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README.md
================================================
# Solid State Deploy

Deploy simple websites with Google Cloud and Cloudflare.
It is like Netlify with:

* **Better performance.** Cloudflare has more features for fast websites
  than Netlify CDN. For instance, HTTP/3 and TLS 1.3 0-RTT.
* **Flexibility.** You can have crontab jobs and simple scripts
  (without persistence storage). You will have powerful and well documented
  Nginx config to define custom headers and redirects.
* **Lack of vendor lock-in.** We use industry standards like Docker
  and Nginx. You can change CI, CDN, or Docker cloud separately.
* **Local tests.** You can run a server on your laptop to test redirects
  and scripts.

You will have built-in HTTPS and deploy by `git push`.

<img src="./example.png" alt="ssdeploy example" width="593">

We also have trade-offs. It is not free, but for a simple website,
it will cost you cents per month. You need more steps to install it,
but after you have the same simple workflow.

<a href="https://evilmartians.com/?utm_source=ssdeploy">
  <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg"
       alt="Sponsored by Evil Martians" width="236" height="54">
</a>

## Install

1. Create an account in [Google Cloud].
2. Go to **IAM & Admin** → **Service Accounts**,
   click **Create Service Account** and fill form:
   * Service Account Name: `Github-actions`
   * Service Account Description: `Deploy from Github Actions`
3. Add **Cloud Run Admin**, **Storage Admin**, **Service Account User** roles.
4. Click **Create Key**, choose **JSON** → **Create**, download and keep
   file for a while.
5. Open **Container Registry** and enable the service.
6. Open **Cloud Run** and start the service.
7. Go to your Github page of your project at **Settings** → **Secrets**.
8. Add new secret `WEBSITE_URL` with URL to your website domain
   (like `example.com`).
9. Add new secret `GCLOUD_PROJECT` with Google Cloud project name like
   `test-255417`. You can find project ID by opening a project switcher
   at the top of [Google Cloud].
10. Choose application name (like `examplecom`) and add `GCLOUD_APP` secret with
   this name.
11. Call `base64 key-partition-….json` (file from step 4) and add `GCLOUD_AUTH`
    secret with the base64 content of this file.
12. Install Solid State Deploy to your project.

    ```sh
    npm i ssdeploy
    ```
13. Create Github Actions workflow by calling:

    ```sh
    npx ssdeploy init
    ```
14. Your project should build HTML files by `npm build` and put them to `dist/`.
15. Push the project’s changes to Github Actions to start deploying.
    Open **Actions** tab on Github to check out the process.
16. Go to **Cloud Run** at [Google Cloud] and find your server. Open it
    by clicking on the name and find the URL like `examplecom-hjv54hv.a.run.app`.
    Check that the website is working.
17. Click on **Manage Custom Domains** → **Add mapping**. Select your app,
    **Verify a new domain**, and enter your domain name.
    Finish domain verification with Webmaster Central.
18. After verification open **Add mapping** dialog again, select your app,
    domain, and leave subdomain blank. You will get `A` and `AAAA` records.
19. Create a new [Cloudflare] account.
    Create a site with `A` and `AAAA` records from Cloud Run.
20. Enable **HTTP/3** and **0-RTT** in Cloudflare **Network** settings.
21. Find **Zone ID** at site overview and create **API token**
    with `cache cleaner` name and `Cache Purge`/`Edit` permission.
22. Use them in `CLOUDFLARE_ZONE` and `CLOUDFLARE_TOKEN` secrets at Github.
23. Go to Google Cloud Run, **Manage Custom Domains** → **Add mapping**
    to add `www` subdomain and add `CNAME` record to Cloudflare **DNS**
    settings.

We recommend checking the final result
[for blocking in Russia](https://isitblockedinrussia.com/) and recreate
Cloudflare account to change IP addressed.

Few extra steps will improve security:

1. Go to Cloudflare **SSL/TLS** settings and enable **Full** encryption mode.
2. Add `CAA` records to Cloudflare **DNS** settings:

   ```js
   CAA @   0 "only allow specific hostname" digicert.com
   CAA @   0 "only allow specific hostname" letsencrypt.org
   CAA www 0 "only allow specific hostname" digicert.com
   CAA www 0 "only allow specific hostname" letsencrypt.org
   ```
3. Enable **DNSSEC** in **DNS** settings.
4. Enable **HTST** by creating `nginx.conf` in the root of your project with:

   ```cpp
   add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
   add_header X-Content-Type-Options "nosniff";
   ```

[Google Cloud]: https://console.cloud.google.com/
[Cloudflare]: https://www.cloudflare.com/


## Deploy

Just push commits to `master`:

```sh
git push origin master
```

You can switch deploy branch at `.Github/workflows/deploy.yml`.


## Run Server Locally

To test the Docker image locally run:

```sh
npm build
npx ssdeploy run
```

## Deploy Server Locally

You can deploy a server from the laptop. It can be useful to debug.

You need to install [Google Cloud SDK](https://cloud.google.com/sdk/install)
and call:

```sh
npx ssdeploy deploy
```


## Custom Nginx config

In custom Nginx config, you can define headers and redirects. Create `nginx.conf`
in your project root.

```cpp
if ($host ~ ^www\.(?<domain>.+)$) {
  return 301 https://$domain$request_uri;
}

location ~* "(\.css|\.png|\.svg|\.woff2)$" {
  add_header Cache-Control "public, max-age=31536000, immutable";
}
```

It will be included inside the `server` context.


## Custom Docker config

Custom `Dockerfile` should be placed at your project root. It can be used
to define crontab jobs:

```sh
FROM nginx:alpine
RUN rm -R /etc/nginx/conf.d
COPY ./dist/ /var/www/
COPY ./node_modules/ssdeploy/configs/nginx.conf /etc/nginx/nginx.template
COPY ./nginx.conf /etc/nginx/server.conf
RUN echo "#\!/bin/sh\necho 1" > /etc/periodic/hourly/example
RUN chmod a+x /etc/periodic/hourly/example
CMD crond && envsubst \$PORT < /etc/nginx/nginx.template > /etc/nginx/nginx.conf && nginx
```


================================================
FILE: bin.js
================================================
#!/usr/bin/env node

import dotenv from 'dotenv'
import pico from 'picocolors'

import deploy, { stopPreview } from './lib/deploy.js'
import showVersion from './lib/show-version.js'
import showHelp from './lib/show-help.js'
import runImage from './lib/run-image.js'
import changed from './lib/changed.js'
import purge from './lib/purge.js'
import init from './lib/init.js'
import sign from './lib/sign.js'

dotenv.config()

function getPullRequestId() {
  let pr = process.argv[3]
  if (!pr) {
    process.stderr.write(pico.red('Missed pull request ID') + '\n')
    process.exit(1)
  }
  return pr
}

async function run() {
  let command = process.argv[2]
  if (command === '--version') {
    await showVersion()
  } else if (!command || command === 'help' || command === '--help') {
    showHelp()
  } else if (command === 'init') {
    await init()
  } else if (command === 'shell') {
    await runImage('/bin/sh')
  } else if (command === 'run') {
    await runImage()
  } else if (command === 'purge') {
    await purge()
  } else if (command === 'changed') {
    await changed()
  } else if (command === 'deploy') {
    await deploy()
  } else if (command === 'preview') {
    await deploy(getPullRequestId())
  } else if (command === 'close') {
    await stopPreview(getPullRequestId())
  } else if (command === 'sign') {
    let file = process.argv[3]
    if (!file) {
      process.stderr.write(pico.red('Missed file to sign') + '\n')
      process.exit(1)
    }
    await sign(file)
  } else {
    process.stderr.write(pico.red(`Unknown command ${command}`) + '\n\n')
    showHelp()
    process.exit(1)
  }
}

run().catch(e => {
  if (!e.own) process.stderr.write(pico.red(e.stack) + '\n')
  process.exit(1)
})


================================================
FILE: configs/.dockerignore
================================================
node_modules/
!node_modules/ssdeploy/configs/
.github/
.cache/
.git/
src/
.gitignore
.editorconfig
README.md
LICENSE
package.json
yarn.lock
pnpm-lock.yaml


================================================
FILE: configs/Dockerfile
================================================
FROM nginx:alpine
RUN rm -R /etc/nginx/conf.d
COPY ./nginx-template.conf /etc/nginx/nginx.template
COPY ./nginx.conf /etc/nginx/server.conf
COPY ./dist/ /var/www/
CMD envsubst \$PORT < /etc/nginx/nginx.template > /etc/nginx/nginx.conf && nginx


================================================
FILE: configs/close.yml
================================================
name: Clean Preview
on:
  pull_request:
    types: [ closed ]
jobs:
  close:
    runs-on: ubuntu-latest
    steps:
      - name: Clean from GitHub
        uses: bobheadxi/deployments@v1
        with:
          step: delete-env
          token: ${{ secrets.GITHUB_TOKEN }}
          env: preview-${{ github.event.number }}
      - name: Checkout the repository
        uses: actions/checkout@v3
      - name: Install Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: npm
      - name: Install dependencies
        run: npm install --production --frozen-lockfile
      - name: Auth Google Cloud
        uses: google-github-actions/auth@v0
        with:
          credentials_json: ${{ secrets.GCLOUD_AUTH }}
      - name: Install Google Cloud
        uses: google-github-actions/setup-gcloud@v0
      - name: Clean from Google Cloud
        run: ./node_modules/.bin/ssdeploy close $PR --verbose
        env:
          PR: ${{ github.event.number }}
          GCLOUD_APP: ${{ secrets.GCLOUD_APP }}
          GCLOUD_PROJECT: ${{ secrets.GCLOUD_PROJECT }}
          CLOUDFLARE_ZONE: ${{ secrets.CLOUDFLARE_ZONE }}
          CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_TOKEN }}


================================================
FILE: configs/deploy.yml
================================================
name: Deploy
on:
  push:
    branches:
      - main
env:
  FORCE_COLOR: 2
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout the repository
        uses: actions/checkout@v3
      - name: Install Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: npm
      - name: Install dependencies
        run: npm install --production --frozen-lockfile
      - name: Build static files
        run: npm build
      - name: Check files changes
        id: hash
        run: ./node_modules/.bin/ssdeploy changed
        env:
          WEBSITE_URL: ${{ secrets.WEBSITE_URL }}
      - name: Auth Google Cloud
        uses: google-github-actions/auth@v0
        with:
          credentials_json: ${{ secrets.GCLOUD_AUTH }}
      - name: Install Google Cloud
        if: "!steps.hash.outputs.noChanges"
        uses: google-github-actions/setup-gcloud@v0
      - name: Deploy files
        if: "!steps.hash.outputs.noChanges"
        run: ./node_modules/.bin/ssdeploy deploy --verbose
        env:
          GCLOUD_APP: ${{ secrets.GCLOUD_APP }}
          GCLOUD_PROJECT: ${{ secrets.GCLOUD_PROJECT }}
          CLOUDFLARE_ZONE: ${{ secrets.CLOUDFLARE_ZONE }}
          CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_TOKEN }}


================================================
FILE: configs/nginx-template.conf
================================================
worker_processes 1;
pid /run/nginx.pid;
daemon off;

events {
  worker_connections 1024;
}

http {
  access_log off;
  error_log stderr error;
  server_tokens off;

  include mime.types;
  types {
    application/manifest+json webmanifest;
  }
  default_type application/octet-stream;
  charset_types application/javascript text/css application/manifest+json image/svg+xml;
  sendfile on;

  server {
    listen $PORT;

    root /var/www;
    charset UTF-8;
    gzip_static on;

    include /etc/nginx/server.conf;
  }
}


================================================
FILE: configs/preview.yml
================================================
name: Preview
on:
  pull_request:
env:
  FORCE_COLOR: 2
jobs:
  preview:
    runs-on: ubuntu-latest
    if: github.ref != 'refs/heads/main'
    steps:
      - name: Notify about new deployment
        uses: bobheadxi/deployments@v1
        id: deployment
        with:
          step: start
          token: ${{ secrets.GITHUB_TOKEN }}
          ref: ${{ github.head_ref }}
          env: preview-${{ github.event.number }}
      - name: Checkout the repository
        uses: actions/checkout@v3
      - name: Install Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: npm
      - name: Install dependencies
        run: npm install --production --frozen-lockfile
      - name: Build static files
        run: npm build
      - name: Check files changes
        id: hash
        run: ./node_modules/.bin/ssdeploy changed
        env:
          WEBSITE_URL: ${{ secrets.WEBSITE_URL }}
      - name: Auth Google Cloud
        uses: google-github-actions/auth@v0
        with:
          credentials_json: ${{ secrets.GCLOUD_AUTH }}
      - name: Install Google Cloud
        uses: google-github-actions/setup-gcloud@v0
      - name: Deploy files
        id: deploy
        run: ./node_modules/.bin/ssdeploy preview $PR --verbose
        env:
          PR: ${{ github.event.number }}
          GCLOUD_APP: ${{ secrets.GCLOUD_APP }}
          GCLOUD_PROJECT: ${{ secrets.GCLOUD_PROJECT }}
          CLOUDFLARE_ZONE: ${{ secrets.CLOUDFLARE_ZONE }}
          CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_TOKEN }}
      - name: Update deployment status
        uses: bobheadxi/deployments@v1
        if: always()
        with:
          step: finish
          token: ${{ secrets.GITHUB_TOKEN }}
          status: ${{ job.status }}
          env: ${{ steps.deployment.outputs.env }}
          env_url: ${{ steps.deploy.outputs.url }}
          deployment_id: ${{ steps.deployment.outputs.deployment_id }}


================================================
FILE: lib/build.js
================================================
import { writeFile, copyFile, unlink } from 'fs/promises'
import { basename, join } from 'path'
import { spawn, exec } from 'child_process'
import { existsSync } from 'fs'
import bytes from 'bytes'
import pico from 'picocolors'

import debug, { debugCmd } from './debug.js'
import { findPackageDir } from './find-package-up.js'
import detectDocker from './detect-docker.js'
import showSpinner from './show-spinner.js'
import { CONFIGS } from './dirs.js'

function getSize(bin, name, env) {
  return new Promise(resolve => {
    exec(
      `${bin} image inspect ${name}:latest --format='{{.Size}}'`,
      { env },
      (error, stdout) => {
        if (error) {
          resolve()
        } else {
          resolve(parseInt(stdout))
        }
      }
    )
  })
}

export default async function build(name, env = process.env, forceDocker) {
  let root = await findPackageDir()
  if (!name) name = basename(root)
  let bin = forceDocker ? 'docker' : await detectDocker()

  let customDocker = join(root, 'Dockerfile')
  let localDocker = join(CONFIGS, 'Dockerfile')
  let customNginxTemplate = join(root, 'nginx-template.conf')
  let localNginxTemplate = join(CONFIGS, 'nginx-template.conf')
  let customIgnore = join(root, '.dockerignore')
  let localIgnore = join(CONFIGS, '.dockerignore')
  let nginxPath = join(root, 'nginx.conf')

  let dockerfile = localDocker
  if (existsSync(customDocker)) dockerfile = customDocker

  let args = ['build', '-f', dockerfile, '-t', name, '.']
  debugCmd(bin + ' ' + args.join(' '))

  let text = 'Building Docker image'
  if (bin === 'podman') text = 'Building Podman image'
  let spinner = showSpinner(text)

  let temp = []
  try {
    if (!existsSync(customIgnore)) {
      await copyFile(localIgnore, customIgnore)
      temp.push(customIgnore)
    }
    if (!existsSync(nginxPath)) {
      await writeFile(nginxPath, '\n')
      temp.push(nginxPath)
    }
    if (!existsSync(customNginxTemplate)) {
      await copyFile(localNginxTemplate, customNginxTemplate)
      temp.push(customNginxTemplate)
    }

    await new Promise((resolve, reject) => {
      let docker = spawn(bin, args, { env })
      let steps = 0
      docker.stdout.on('data', data => {
        if (!process.env.CI && !process.env.GITHUB_ACTIONS) {
          if (data.toString().startsWith('STEP ')) {
            steps += 1
            process.stdout.write(`${text}: step ${steps}\n`)
          }
        }
        debug(data)
      })
      let output = ''
      docker.stderr.on('data', data => {
        output += data.toString()
      })
      docker.on('exit', code => {
        if (code === 0) {
          resolve()
        } else {
          spinner.fail()
          process.stderr.write(pico.red(output))
          let err = new Error('Docker error ' + code)
          err.own = true
          reject(err)
        }
      })
    })
  } finally {
    await Promise.all(temp.map(i => unlink(i)))
  }

  let size = await getSize(bin, name, env)
  if (size) {
    spinner.succeed(`Built ${bytes(size, { unitSeparator: ' ' })} image`)
  }

  return name
}


================================================
FILE: lib/call-cloudflare.js
================================================
import { request } from 'https'

export default async function callCloudflare (command, opts) {
  await new Promise((resolve, reject) => {
    let req = request(
      {
        method: 'POST',
        hostname: 'api.cloudflare.com',
        path: `/client/v4/zones/${process.env.CLOUDFLARE_ZONE}/${command}`,
        headers: {
          'Authorization': `Bearer ${process.env.CLOUDFLARE_TOKEN}`,
          'Content-Type': 'application/json'
        }
      },
      res => {
        let data = ''
        res.on('data', chunk => {
          data += chunk
        })
        res.on('end', () => {
          let answer
          try {
            answer = JSON.parse(data)
          } catch {
            console.error(data)
            process.exit(1)
          }
          if (answer.success) {
            resolve()
          } else {
            reject(new Error(answer.errors[0].message))
          }
        })
      }
    )
    if (opts) req.write(JSON.stringify(opts))
    req.on('error', reject)
    req.end()
  })
}


================================================
FILE: lib/changed.js
================================================
import { existsSync, createReadStream } from 'fs'
import { writeFile } from 'fs/promises'
import folderHash from 'folder-hash'
import { join } from 'path'
import { get } from 'https'
import hasha from 'hasha'
import pico from 'picocolors'

import showError, { showWarning } from './show-error.js'
import showSpinner, { wrap } from './show-spinner.js'
import { findPackageDir } from './find-package-up.js'

async function fileHash(file) {
  if (!existsSync(file)) return ''
  return hasha.fromStream(createReadStream(file))
}

async function loadPrev(url) {
  if (url.startsWith('http://')) {
    throw showError('`ssdeploy changed` supports only https:// websites')
  }
  if (!url.startsWith('https://')) {
    url = 'https://' + url
  }

  try {
    new URL(url)
  } catch (e) {
    if (e.code === 'ERR_INVALID_URL') {
      throw showError(
        'Ivalid URL `' + url + '` in `WEBSITE_URL` environment variables'
      )
    } else {
      throw e
    }
  }

  if (!url.endsWith('/')) url += '/'
  let hashUrl = url + 'ssdeploy-hash.txt'

  let spinner = showSpinner('Loading hash from previous deploy')

  return new Promise(resolve => {
    function error(e) {
      spinner.fail()
      if (typeof e === 'string') {
        showWarning(e)
      } else {
        showWarning(e.message)
      }
      resolve('')
    }

    let req = get(hashUrl, res => {
      if (res.statusCode === 404) {
        error(
          '`' +
            hashUrl +
            '` file was not found.\n' +
            'It is OK for first deploy.'
        )
        return
      } else if (res.statusCode !== 200) {
        error(
          'Your website return ' +
            res.statusCode +
            ' code ' +
            'on `' +
            hashUrl +
            '` request'
        )
      }
      let data = ''
      res.on('data', chunk => {
        data += chunk.toString()
      })
      res.on('end', () => {
        if (data.endsWith('=')) {
          spinner.succeed()
          resolve(data)
        } else {
          error('Wrong data found at `' + hashUrl + '`')
        }
      })
    })
    req.on('error', error)
    req.end()
  })
}

async function calcCurrent() {
  let root = await findPackageDir()
  let dist = join(root, 'dist')
  let ignoreFile = join(root, '.dockerignore')
  let dockerFile = join(root, 'Dockerfile')
  let nginxFile = join(root, 'nginx.conf')
  let workflowFile = join(root, '.github', 'workflows', 'deploy.yml')
  let [{ hash }, workflow, ignore, docker, nginx] = await Promise.all([
    folderHash.hashElement(dist),
    fileHash(workflowFile),
    fileHash(ignoreFile),
    fileHash(dockerFile),
    fileHash(nginxFile)
  ])
  return [dist, hasha(hash + workflow + ignore + docker + nginx) + '=']
}

export default async function changed() {
  if (!process.env.WEBSITE_URL) {
    showWarning(
      'Set `WEBSITE_URL` environment variables at your CI ',
      'to deploy websites only when files will be changed'
    )
    return
  }

  let [prev, [dist, current]] = await Promise.all([
    loadPrev(process.env.WEBSITE_URL.trim()),
    calcCurrent()
  ])
  await wrap('Writing ssdeploy-hash.txt', async () => {
    await writeFile(join(dist, 'ssdeploy-hash.txt'), current)
  })
  if (prev === current) {
    process.stderr.write(
      pico.yellow('Files were not changed from latest deploy. Stop deploy.\n')
    )
    await writeFile(process.env.GITHUB_OUTPUT, 'noChanges=1\n')
  } else {
    if (prev !== '') {
      process.stderr.write('Files was changed. ')
    }
    process.stderr.write('Continue deploy.\n')
  }
}


================================================
FILE: lib/debug.js
================================================
import pico from 'picocolors'

export let isDebug = process.argv.includes('--verbose')

export function debugCmd(cmd) {
  debug(pico.gray('$ ' + cmd) + '\n')
}

export default function debug(text) {
  if (isDebug) {
    process.stderr.write(text)
  }
}


================================================
FILE: lib/deploy.js
================================================
import { spawn, execSync } from 'child_process'
import { writeFile } from 'fs/promises'
import pico from 'picocolors'

import debug, { isDebug, debugCmd } from './debug.js'
import { wrap } from './show-spinner.js'
import showError from './show-error.js'
import build from './build.js'
import purge from './purge.js'

let safeEnv = {}
for (let i in process.env) {
  if (!/^(GCLOUD_|CLOUDFLARE_)/.test(i)) safeEnv[i] = process.env[i]
}

async function exec(command, opts) {
  return new Promise((resolve, reject) => {
    debugCmd(command)
    let cmd = spawn(command, { ...opts, env: safeEnv, shell: '/bin/bash' })
    let stdout = ''
    let stderr = ''
    cmd.stdout.on('data', data => {
      let out = data.toString()
      stdout += out
      debug(out)
    })
    cmd.stderr.on('data', data => {
      stderr += data.toString()
      debug(pico.yellow(data))
    })
    cmd.on('exit', code => {
      if (code === 0) {
        resolve(stdout.trim())
      } else if (isDebug) {
        reject(showError('Exit code ' + code))
      } else {
        reject(showError(stderr))
      }
    })
  })
}

async function push(image) {
  await wrap('Pushing image to Google Cloud Registry', async () => {
    await exec('gcloud auth configure-docker --quiet')
    await exec(`docker push ${image}`)
  })
}

async function setPreviewUrl(project, app, region = 'us-east1', preview) {
  let base = execSync(
    `gcloud run services describe ${app} ` +
      `--project ${project} --region ${region} --format 'value(status.url)'`
  )
    .toString()
    .trim()
  let url = base.replace('https://', `https://preview-${preview}---`)
  await writeFile(process.env.GITHUB_OUTPUT, `url=${url}\n`)
}

async function run(image, project, app, region, preview) {
  await wrap('Starting image on Google Cloud Run', async spinner => {
    await exec(
      `gcloud run deploy ${app} --image ${image} ` +
        `--project ${project} --region=${region} ` +
        '--platform managed --allow-unauthenticated' +
        (preview ? ` --tag preview-${preview} --no-traffic` : '')
    )
    spinner.succeed(`Image was deployed at ${region} server`)
  })
}

async function cleanOldImages(image, tag) {
  await wrap('Cleaning registry from old images', async spinner => {
    let removed = 0
    let filter = '-tags:*'
    if (tag) filter = `tags:${tag}`
    let out = await exec(
      'gcloud container images list-tags ' +
        `${image} --filter='${filter}' --format='get(digest)'`
    )
    if (out !== '') {
      await Promise.all(
        out.split('\n').map(i => {
          removed += 1
          return exec(
            `gcloud container images delete '${image}@${i}' --quiet` +
              (tag ? '  --force-delete-tags' : '')
          )
        })
      )
    }
    spinner.succeed(`Removed ${removed} images`)
  })
}

async function migrateTraffic(project, app, region) {
  await wrap('Moving traffic to new revision', async () => {
    await exec(
      'gcloud run services update-traffic ' +
        `${app} --project ${project} --region=${region} --to-latest`
    )
  })
}

export default async function deploy(preview) {
  if (!process.env.GCLOUD_PROJECT || !process.env.GCLOUD_APP) {
    throw showError(
      'Set `GCLOUD_PROJECT` and `GCLOUD_APP` environment variables at your CI'
    )
  }

  let project = process.env.GCLOUD_PROJECT.trim()
  let app = process.env.GCLOUD_APP.trim()
  let region = process.env.GCLOUD_REGION || 'us-east1'
  let image = `gcr.io/${project}/${app}`
  let taggedImage = preview ? `${image}:preview-${preview}` : image

  await build(taggedImage, safeEnv, true, preview)
  await push(taggedImage)
  await run(taggedImage, project, app, region, preview)
  if (preview) {
    await setPreviewUrl(project, app, region, preview)
  } else {
    await migrateTraffic(project, app, region)
    await purge()
  }
  await cleanOldImages(image)
}

export async function stopPreview(preview) {
  let project = process.env.GCLOUD_PROJECT.trim()
  let app = process.env.GCLOUD_APP.trim()
  let image = `gcr.io/${project}/${app}`
  let region = process.env.GCLOUD_REGION || 'us-east1'

  await exec(
    `gcloud run services update-traffic ${app} ` +
      `--region ${region} --project ${project} ` +
      `--remove-tags preview-${preview} --async`
  )
  await cleanOldImages(image, `preview-${preview}`)
}


================================================
FILE: lib/detect-docker.js
================================================
import { promisify } from 'util'
import child from 'child_process'

let exec = promisify(child.exec)
let runner

export default async function detectDocker () {
  if (!runner) {
    try {
      await exec('podman --version')
      runner = 'podman'
    } catch (e) {
      runner = 'docker'
    }
  }
  return runner
}


================================================
FILE: lib/dirs.js
================================================
import { dirname, join } from 'path'

let self = new URL(import.meta.url).pathname

export const ROOT = join(dirname(self), '..')
export const CONFIGS = join(ROOT, 'configs')


================================================
FILE: lib/find-package-up.js
================================================
import { resolve, parse, dirname } from 'path'
import { existsSync } from 'fs'

import showError from './show-error.js'

async function findUp(name, cwd = '') {
  let directory = resolve(cwd)
  let { root } = parse(directory)

  while (true) {
    let foundPath = await resolve(directory, name)

    if (existsSync(foundPath)) {
      return foundPath
    }

    if (directory === root) {
      return undefined
    }

    directory = dirname(directory)
  }
}

export async function findPackageUp(cwd) {
  let path = await findUp('package.json', cwd)
  if (!path) throw showError('Can’t find package.json')
  return path
}

export async function findPackageDir(cwd) {
  return dirname(await findPackageUp(cwd))
}


================================================
FILE: lib/init.js
================================================
import { readFile, mkdir, writeFile } from 'fs/promises'
import { dirname, join } from 'path'
import { existsSync } from 'fs'

import { findPackageDir } from './find-package-up.js'
import { CONFIGS } from './dirs.js'

async function copyTemplate(manager, name, to) {
  await mkdir(dirname(to), { recursive: true })
  let template = await readFile(join(CONFIGS, name))

  let config = template.toString()
  if (manager === 'yarn') {
    config = config
      .replace('cache: npm', 'cache: yarn')
      .replace(' npm ', ' yarn ')
  } else if (manager === 'pnpm') {
    config = config
      .replace('cache: npm', 'cache: pnpm')
      .replace(' npm ', ' pnpm ')
  }

  await writeFile(join(to, name), config)
}

export default async function init() {
  let root = await findPackageDir()
  let workflows = join(root, '.github', 'workflows')

  let manager = 'npm'
  if (existsSync(join(root, 'yarn.lock'))) {
    manager = 'yarn'
  } else if (existsSync(join(root, 'pnpm-lock.yaml'))) {
    manager = 'pnpm'
  }

  await Promise.all([
    copyTemplate(manager, workflows, 'deploy.yml'),
    copyTemplate(manager, workflows, 'preview.yml'),
    copyTemplate(manager, workflows, 'close.yml')
  ])
}


================================================
FILE: lib/purge.js
================================================
import callCloudflare from './call-cloudflare.js'
import { showWarning } from './show-error.js'
import { wrap } from './show-spinner.js'

export default async function purge () {
  if (!process.env.CLOUDFLARE_ZONE || !process.env.CLOUDFLARE_TOKEN) {
    showWarning(
      'Get zone ID and API token in CLoudflare dashboard',
      'and set `CLOUDFLARE_TOKEN` and `CLOUDFLARE_ZONE`',
      'environment variables at your CI'
    )
  } else {
    await wrap('Cleaning CDN cache', async spinner => {
      await callCloudflare('purge_cache', { purge_everything: true })
      spinner.succeed('CDN cache was cleaned')
    })
  }
}


================================================
FILE: lib/run-image.js
================================================
import { existsSync } from 'fs'
import { spawn } from 'child_process'
import { join } from 'path'
import open from 'open'
import pico from 'picocolors'

import { findPackageDir } from './find-package-up.js'
import detectDocker from './detect-docker.js'
import { debugCmd } from './debug.js'
import build from './build.js'

let y = pico.yellow

export default async function runImage(script) {
  let bin = await detectDocker()
  let name = await build()
  let root = await findPackageDir()
  let args = ['run']
  if (existsSync(join(root, 'dist'))) {
    args = args.concat(['-v', './dist/:/var/www/'])
  }
  args = args.concat([
    '--privileged',
    '--rm',
    '-p',
    '8000:80',
    '-e',
    'PORT=80',
    '-it',
    name
  ])
  if (script) args.push(script)
  debugCmd(bin + ' ' + args.join(' '))
  spawn(bin, args, { stdio: 'inherit' })
  if (!script) {
    let ctrlC = 'Ctrl+C'
    if (process.platform === 'darwin') ctrlC = 'Cmd + .'
    process.stderr.write(
      `  Website is available to test at ${y('http://localhost:8000/')}\n` +
        `  Press ${y(ctrlC)} to stop the server\n`
    )
    open('http://localhost:8000/')
  }
}


================================================
FILE: lib/show-error.js
================================================
import pico from 'picocolors'

export function showWarning(...lines) {
  lines = lines.map(line => {
    return line.replace(/`[^`]+`/g, i => pico.yellow(i.slice(1, -1)))
  })
  process.stderr.write(pico.red(lines.join('\n')) + '\n')
}

export default function showError(...lines) {
  showWarning(...lines)
  let err = new Error('show error')
  err.own = true
  return err
}


================================================
FILE: lib/show-help.js
================================================
import pico from 'picocolors'

let b = pico.bold
let y = pico.yellow
let g = pico.green

function print(...lines) {
  process.stdout.write(lines.join('\n') + '\n')
}

export default function showHelp() {
  print(
    b('Usage: ') + 'npx ssdeploy ' + g('COMMAND') + y(' [OPTION]'),
    'Deploy simple websites with Google Cloud and Cloudflare',
    '',
    b('Commands:'),
    '  ' + g('init') + '        Create .github/workflow/deploy.yml',
    '  ' + g('deploy') + '      Deploy website to the cloud',
    '  ' + g('preview') + ' ' + y('PR') + '  Deploy preview',
    '  ' + g('close') + ' ' + y('PR') + '    Clean files from preview',
    '  ' + g('run') + '         Run Docker image locally',
    '  ' + g('shell') + '       Run shell inside Docker image',
    '  ' + g('purge') + '       Clean CDN cache',
    '  ' + g('changed') + '     Check changes between dist/ and website',
    '  ' + g('sign') + ' ' + y('FILE') + '   Sign security.txt',
    '',
    b('Arguments:'),
    '  ' + y('--verbose') + '   Show debug information',
    '',
    b('Examples:'),
    '  npx ssdeploy deploy',
    '  npx ssdeploy run',
    '  npx ssdeploy sign ./src/well-known/security.txt'
  )
}


================================================
FILE: lib/show-spinner.js
================================================
import pico from 'picocolors'

export default function showSpinner(text) {
  process.stdout.write(pico.gray('- ') + text + '\n')
  let finished = false
  return {
    succeed(newText = text) {
      if (!finished) {
        finished = true
        process.stdout.write(pico.green('✔ ') + newText + '\n')
      }
    },
    fail(newText = text) {
      if (!finished) {
        finished = true
        process.stdout.write(pico.red('✖ ') + newText + '\n')
      }
    }
  }
}

export async function wrap(text, cb) {
  let spinner = showSpinner(text)
  try {
    let result = await cb(spinner)
    spinner.succeed()
    return result
  } catch (e) {
    spinner.fail()
    throw e
  }
}


================================================
FILE: lib/show-version.js
================================================
import { readFile } from 'fs/promises'
import { join } from 'path'
import pico from 'picocolors'

import { ROOT } from './dirs.js'

export default async function showVersion() {
  let packagePath = join(ROOT, 'package.json')
  let packageJson = await readFile(packagePath)
  let { version } = JSON.parse(packageJson)
  process.stdout.write(`ssdeploy ${pico.bold(version)}\n`)
}


================================================
FILE: lib/sign.js
================================================
import { writeFile, unlink, rename, readFile } from 'fs/promises'
import { promisify } from 'util'
import child from 'child_process'

let exec = promisify(child.exec)

export default async function sign(path) {
  let content = await readFile(path)
  let cleared = content
    .toString()
    .replace(/-----BEGIN PGP SIGNED MESSAGE-----\nHash:[^\n]+\n\n/, '')
    .replace(/-----BEGIN PGP SIGNATURE-----\n\n[\W\w]+$/m, '')
  await writeFile(path, cleared)
  await exec(`gpg --clearsign ${path}`)
  await unlink(path)
  await rename(path + '.asc', path)
}


================================================
FILE: package.json
================================================
{
  "name": "ssdeploy",
  "version": "0.9.3",
  "description": "Netlify replacement to deploy simple websites with better flexibility and speed and without vendor lock-in",
  "keywords": [
    "deploy",
    "google cloud",
    "cloudflare",
    "netlify"
  ],
  "bin": "./bin.js",
  "type": "module",
  "engines": {
    "node": ">=14.0.0"
  },
  "scripts": {
    "test": "eslint ."
  },
  "dependencies": {
    "bytes": "^3.1.2",
    "dotenv": "^16.0.3",
    "folder-hash": "^4.0.4",
    "hasha": "^5.2.2",
    "open": "^8.4.2",
    "picocolors": "^1.0.0"
  },
  "author": "Andrey Sitnik <andrey@sitnik.ru>",
  "license": "MIT",
  "repository": "ai/ssdeploy",
  "devDependencies": {
    "@logux/eslint-config": "^49.0.0",
    "clean-publish": "^4.1.1",
    "eslint": "^8.36.0",
    "eslint-config-standard": "^17.0.0",
    "eslint-plugin-import": "^2.27.5",
    "eslint-plugin-n": "^15.6.1",
    "eslint-plugin-prefer-let": "^3.0.1",
    "eslint-plugin-promise": "^6.1.1"
  },
  "eslintConfig": {
    "extends": "@logux/eslint-config",
    "rules": {
      "no-console": "off"
    }
  },
  "eslintIgnore": [
    "lib/dirs.js"
  ],
  "prettier": {
    "arrowParens": "avoid",
    "jsxSingleQuote": false,
    "quoteProps": "consistent",
    "semi": false,
    "singleQuote": true,
    "trailingComma": "none"
  },
  "clean-publish": {
    "cleanDocs": true
  }
}


================================================
FILE: purge.js
================================================
#!/usr/bin/env node

import callCloudflare from './lib/call-cloudflare.js'

callCloudflare('purge_cache', { purge_everything: true }).catch(e => {
  process.stderr.write(e.stack + '\n')
  process.exit(1)
})
Download .txt
gitextract_gk28koth/

├── .editorconfig
├── .github/
│   └── workflows/
│       └── test.yml
├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── bin.js
├── configs/
│   ├── .dockerignore
│   ├── Dockerfile
│   ├── close.yml
│   ├── deploy.yml
│   ├── nginx-template.conf
│   └── preview.yml
├── lib/
│   ├── build.js
│   ├── call-cloudflare.js
│   ├── changed.js
│   ├── debug.js
│   ├── deploy.js
│   ├── detect-docker.js
│   ├── dirs.js
│   ├── find-package-up.js
│   ├── init.js
│   ├── purge.js
│   ├── run-image.js
│   ├── show-error.js
│   ├── show-help.js
│   ├── show-spinner.js
│   ├── show-version.js
│   └── sign.js
├── package.json
└── purge.js
Download .txt
SYMBOL INDEX (37 symbols across 17 files)

FILE: bin.js
  function getPullRequestId (line 17) | function getPullRequestId() {
  function run (line 26) | async function run() {

FILE: lib/build.js
  function getSize (line 14) | function getSize(bin, name, env) {
  function build (line 30) | async function build(name, env = process.env, forceDocker) {

FILE: lib/call-cloudflare.js
  function callCloudflare (line 3) | async function callCloudflare (command, opts) {

FILE: lib/changed.js
  function fileHash (line 13) | async function fileHash(file) {
  function loadPrev (line 18) | async function loadPrev(url) {
  function calcCurrent (line 91) | async function calcCurrent() {
  function changed (line 108) | async function changed() {

FILE: lib/debug.js
  function debugCmd (line 5) | function debugCmd(cmd) {
  function debug (line 9) | function debug(text) {

FILE: lib/deploy.js
  function exec (line 16) | async function exec(command, opts) {
  function push (line 43) | async function push(image) {
  function setPreviewUrl (line 50) | async function setPreviewUrl(project, app, region = 'us-east1', preview) {
  function run (line 61) | async function run(image, project, app, region, preview) {
  function cleanOldImages (line 73) | async function cleanOldImages(image, tag) {
  function migrateTraffic (line 97) | async function migrateTraffic(project, app, region) {
  function deploy (line 106) | async function deploy(preview) {
  function stopPreview (line 131) | async function stopPreview(preview) {

FILE: lib/detect-docker.js
  function detectDocker (line 7) | async function detectDocker () {

FILE: lib/dirs.js
  constant ROOT (line 5) | const ROOT = join(dirname(self), '..')
  constant CONFIGS (line 6) | const CONFIGS = join(ROOT, 'configs')

FILE: lib/find-package-up.js
  function findUp (line 6) | async function findUp(name, cwd = '') {
  function findPackageUp (line 25) | async function findPackageUp(cwd) {
  function findPackageDir (line 31) | async function findPackageDir(cwd) {

FILE: lib/init.js
  function copyTemplate (line 8) | async function copyTemplate(manager, name, to) {
  function init (line 26) | async function init() {

FILE: lib/purge.js
  function purge (line 5) | async function purge () {

FILE: lib/run-image.js
  function runImage (line 14) | async function runImage(script) {

FILE: lib/show-error.js
  function showWarning (line 3) | function showWarning(...lines) {
  function showError (line 10) | function showError(...lines) {

FILE: lib/show-help.js
  function print (line 7) | function print(...lines) {
  function showHelp (line 11) | function showHelp() {

FILE: lib/show-spinner.js
  function showSpinner (line 3) | function showSpinner(text) {
  function wrap (line 22) | async function wrap(text, cb) {

FILE: lib/show-version.js
  function showVersion (line 7) | async function showVersion() {

FILE: lib/sign.js
  function sign (line 7) | async function sign(path) {
Condensed preview — 32 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (42K chars).
[
  {
    "path": ".editorconfig",
    "chars": 147,
    "preview": "root = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ni"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 596,
    "preview": "name: Test\non:\n  push:\n    branches:\n      - main\n  pull_request:\npermissions:\n  contents: read\njobs:\n  test:\n    name: "
  },
  {
    "path": ".gitignore",
    "chars": 14,
    "preview": "node_modules/\n"
  },
  {
    "path": ".npmignore",
    "chars": 28,
    "preview": "pnpm-lock.yaml\n\nexample.png\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 2828,
    "preview": "# Change Log\nThis project adheres to [Semantic Versioning](http://semver.org/).\n\n## 0.9.3\n* Move to the new Github Actio"
  },
  {
    "path": "LICENSE",
    "chars": 1095,
    "preview": "The MIT License (MIT)\n\nCopyright 2019 Andrey Sitnik <andrey@sitnik.ru>\n\nPermission is hereby granted, free of charge, to"
  },
  {
    "path": "README.md",
    "chars": 6009,
    "preview": "# Solid State Deploy\n\nDeploy simple websites with Google Cloud and Cloudflare.\nIt is like Netlify with:\n\n* **Better perf"
  },
  {
    "path": "bin.js",
    "chars": 1720,
    "preview": "#!/usr/bin/env node\n\nimport dotenv from 'dotenv'\nimport pico from 'picocolors'\n\nimport deploy, { stopPreview } from './l"
  },
  {
    "path": "configs/.dockerignore",
    "chars": 155,
    "preview": "node_modules/\n!node_modules/ssdeploy/configs/\n.github/\n.cache/\n.git/\nsrc/\n.gitignore\n.editorconfig\nREADME.md\nLICENSE\npac"
  },
  {
    "path": "configs/Dockerfile",
    "chars": 244,
    "preview": "FROM nginx:alpine\nRUN rm -R /etc/nginx/conf.d\nCOPY ./nginx-template.conf /etc/nginx/nginx.template\nCOPY ./nginx.conf /et"
  },
  {
    "path": "configs/close.yml",
    "chars": 1221,
    "preview": "name: Clean Preview\non:\n  pull_request:\n    types: [ closed ]\njobs:\n  close:\n    runs-on: ubuntu-latest\n    steps:\n     "
  },
  {
    "path": "configs/deploy.yml",
    "chars": 1280,
    "preview": "name: Deploy\non:\n  push:\n    branches:\n      - main\nenv:\n  FORCE_COLOR: 2\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n   "
  },
  {
    "path": "configs/nginx-template.conf",
    "chars": 521,
    "preview": "worker_processes 1;\npid /run/nginx.pid;\ndaemon off;\n\nevents {\n  worker_connections 1024;\n}\n\nhttp {\n  access_log off;\n  e"
  },
  {
    "path": "configs/preview.yml",
    "chars": 1943,
    "preview": "name: Preview\non:\n  pull_request:\nenv:\n  FORCE_COLOR: 2\njobs:\n  preview:\n    runs-on: ubuntu-latest\n    if: github.ref !"
  },
  {
    "path": "lib/build.js",
    "chars": 3078,
    "preview": "import { writeFile, copyFile, unlink } from 'fs/promises'\nimport { basename, join } from 'path'\nimport { spawn, exec } f"
  },
  {
    "path": "lib/call-cloudflare.js",
    "chars": 1026,
    "preview": "import { request } from 'https'\n\nexport default async function callCloudflare (command, opts) {\n  await new Promise((res"
  },
  {
    "path": "lib/changed.js",
    "chars": 3559,
    "preview": "import { existsSync, createReadStream } from 'fs'\nimport { writeFile } from 'fs/promises'\nimport folderHash from 'folder"
  },
  {
    "path": "lib/debug.js",
    "chars": 253,
    "preview": "import pico from 'picocolors'\n\nexport let isDebug = process.argv.includes('--verbose')\n\nexport function debugCmd(cmd) {\n"
  },
  {
    "path": "lib/deploy.js",
    "chars": 4332,
    "preview": "import { spawn, execSync } from 'child_process'\nimport { writeFile } from 'fs/promises'\nimport pico from 'picocolors'\n\ni"
  },
  {
    "path": "lib/detect-docker.js",
    "chars": 319,
    "preview": "import { promisify } from 'util'\nimport child from 'child_process'\n\nlet exec = promisify(child.exec)\nlet runner\n\nexport "
  },
  {
    "path": "lib/dirs.js",
    "chars": 175,
    "preview": "import { dirname, join } from 'path'\n\nlet self = new URL(import.meta.url).pathname\n\nexport const ROOT = join(dirname(sel"
  },
  {
    "path": "lib/find-package-up.js",
    "chars": 713,
    "preview": "import { resolve, parse, dirname } from 'path'\nimport { existsSync } from 'fs'\n\nimport showError from './show-error.js'\n"
  },
  {
    "path": "lib/init.js",
    "chars": 1197,
    "preview": "import { readFile, mkdir, writeFile } from 'fs/promises'\nimport { dirname, join } from 'path'\nimport { existsSync } from"
  },
  {
    "path": "lib/purge.js",
    "chars": 628,
    "preview": "import callCloudflare from './call-cloudflare.js'\nimport { showWarning } from './show-error.js'\nimport { wrap } from './"
  },
  {
    "path": "lib/run-image.js",
    "chars": 1148,
    "preview": "import { existsSync } from 'fs'\nimport { spawn } from 'child_process'\nimport { join } from 'path'\nimport open from 'open"
  },
  {
    "path": "lib/show-error.js",
    "chars": 375,
    "preview": "import pico from 'picocolors'\n\nexport function showWarning(...lines) {\n  lines = lines.map(line => {\n    return line.rep"
  },
  {
    "path": "lib/show-help.js",
    "chars": 1180,
    "preview": "import pico from 'picocolors'\n\nlet b = pico.bold\nlet y = pico.yellow\nlet g = pico.green\n\nfunction print(...lines) {\n  pr"
  },
  {
    "path": "lib/show-spinner.js",
    "chars": 685,
    "preview": "import pico from 'picocolors'\n\nexport default function showSpinner(text) {\n  process.stdout.write(pico.gray('- ') + text"
  },
  {
    "path": "lib/show-version.js",
    "chars": 378,
    "preview": "import { readFile } from 'fs/promises'\nimport { join } from 'path'\nimport pico from 'picocolors'\n\nimport { ROOT } from '"
  },
  {
    "path": "lib/sign.js",
    "chars": 555,
    "preview": "import { writeFile, unlink, rename, readFile } from 'fs/promises'\nimport { promisify } from 'util'\nimport child from 'ch"
  },
  {
    "path": "package.json",
    "chars": 1362,
    "preview": "{\n  \"name\": \"ssdeploy\",\n  \"version\": \"0.9.3\",\n  \"description\": \"Netlify replacement to deploy simple websites with bette"
  },
  {
    "path": "purge.js",
    "chars": 207,
    "preview": "#!/usr/bin/env node\n\nimport callCloudflare from './lib/call-cloudflare.js'\n\ncallCloudflare('purge_cache', { purge_everyt"
  }
]

About this extraction

This page contains the full source code of the ai/ssdeploy GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 32 files (38.1 KB), approximately 11.1k tokens, and a symbol index with 37 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!