From 7e8c42eecb9148a4d7f08b2b65025a9a36a3ce08 Mon Sep 17 00:00:00 2001 From: Dafnik Date: Tue, 21 May 2024 23:32:03 +0200 Subject: [PATCH] feat: add ssp action --- .github/SECURITY.md | 17 +++ .github/release-drafter.yml | 40 +++++++ .github/workflows/draft-release.yml | 23 ++++ .github/workflows/release.yml | 29 +++++ .gitignore | 2 + README.md | 163 +++++++++++++++++++++++++++- action.yml | 103 ++++++++++++++++++ renovate.json | 30 +++++ 8 files changed, 406 insertions(+), 1 deletion(-) create mode 100644 .github/SECURITY.md create mode 100644 .github/release-drafter.yml create mode 100644 .github/workflows/draft-release.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 action.yml create mode 100644 renovate.json diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..9bbaa75 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,17 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +|---------|--------------------| +| 1.x.x | :white_check_mark: | + +## Reporting a Vulnerability + +If you believe you've identified a security vulnerability in `ssp` (a bug that allows something to happen that shouldn't be possible), you can reach us at . + +You will receive a response from us within 48 hours. If the issue is confirmed, we will release a patch as soon as possible depending on complexity but likely within a few days. + +## Scope + +A "vulnerability in ssp" is a vulnerability in the code distributed through our main source code repository on GitHub. Vulnerabilities that are specific to a given installation (e.g. misconfiguration) should be reported to the owner of that installation and not us. diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..64ca028 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,40 @@ +--- +name-template: "v$RESOLVED_VERSION" +tag-template: "v$RESOLVED_VERSION" +template: | + # Changelog + + $CHANGES + + See details of [all code changes](https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION) since previous release. +categories: + - title: "🚀 Features" + labels: + - "feature" + - "enhancement" + - title: "🐛 Bug Fixes" + labels: + - "fix" + - "bugfix" + - "bug" + - title: "🧰 Maintenance" + labels: + - "infrastructure" + - "automation" + - "documentation" + - title: "🏎 Performance" + label: "performance" +change-template: "- $TITLE @$AUTHOR (#$NUMBER)" +version-resolver: + major: + labels: + - "type: breaking" + minor: + labels: + - "type: enhancement" + patch: + labels: + - "type: bug" + - "type: maintenance" + - "type: documentation" + default: patch \ No newline at end of file diff --git a/.github/workflows/draft-release.yml b/.github/workflows/draft-release.yml new file mode 100644 index 0000000..77da590 --- /dev/null +++ b/.github/workflows/draft-release.yml @@ -0,0 +1,23 @@ +name: Draft release +on: + push: + branches: + - main + +permissions: + contents: read + +jobs: + draft-release: + permissions: + # write permission is required to create a github release + contents: write + # write permission is required for autolabeler + # otherwise, read permission is required at least + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: release-drafter/release-drafter@v6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ba8ad84 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,29 @@ +name: Release +on: + release: + types: [released] + workflow_dispatch: + inputs: + TAG_NAME: + description: "Tag name that the major tag will point to" + required: true + +env: + TAG_NAME: ${{ github.event.inputs.TAG_NAME || github.event.release.tag_name }} + +permissions: + contents: write + +jobs: + update_tag: + name: Update the major tag to include the ${{ github.event.inputs.TAG_NAME || github.event.release.tag_name }} changes + runs-on: ubuntu-latest + environment: + # Note: this environment is protected + name: Release + steps: + - name: Update the ${{ env.TAG_NAME }} tag + id: update-major-tag + uses: actions/publish-action@v0.3.0 + with: + source-tag: ${{ env.TAG_NAME }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..351e391 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.idea +/.idea/** \ No newline at end of file diff --git a/README.md b/README.md index e8fd842..cfe3768 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,163 @@ -# ssp +# Static Site Preview (SSP) Deploy static site previews to a self-hosted server via ssh + +## Please note that this is in no way secure a solution for hosting static site previews. +- You should only use this in repositories you **trust**. +- You should only use this on a server + - you **DO NOT** care about. + - where you **DO NOT** have sensitive information stored. + - where you **DO NOT** run other sensitive services. +- Unless you jail the specific ssh user, you are allowing a GitHub Action full ssh access to your server. +- You allow any other repository with access to the same preview server to overwrite each others previews. (This should not happen under normal circumstances, as the GitHub Action uses a hash of repository name + pull request number) + +## Usage + +### GitHub Action +```yml +name: preview + +on: [pull_request] + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + pull-requests: write + actions: read + + # Deploy to the preview environment + environment: + name: preview-${{ github.event.number }} + url: ${{ steps.deploy-preview.outputs.url }} + steps: + - uses: actions/download-artifact@v4 + with: + path: ./dist + + - name: deploy preview + id: deploy-preview + uses: dafnik/ssp@v1 + # with: + # source: dist/* + # target: /var/www/preview + # host: preview.yxz.abc + # port: 22 + # username: ubuntu + # key: ${{ secrets.PREVIEW_SSH_PRIVATE_KEY }} + # strip_components: 0 +``` + +| Inputs | Default value | Required | Description | +|--------------------|---------------|----------|--------------------------------------------------------------------------------| +| `source` | | x | Path to the files which should be deployed | +| `target` | | x | Preview server target path, must be a directory path. | +| `host` | | x | Preview server domain | +| `port` | `22` | | Preview server ssh port | +| `username` | | x | Preview server ssh username | +| `key` | | x | Preview server ssh key content of private key. ex raw content of ~/.ssh/id_rsa | +| `strip_components` | `0` | | remove the specified number of leading path elements | + + +Furthermore, see [action.yml](action.yml) + +### NGINX Configuration +Preview are going to stored in the `/var/www/preview` directory. +``` +/etc/nginx/site-enabled/preview +``` + +``` +server { + listen 80; + server_name *.preview.xyz.abc; + return 301 https://$server_name$request_uri; +} + + +server { + listen 443 ssl http2; + server_name ~^(?P.+)\.preview\.xyz\.abc$; + + ssl_certificate /etc/letsencrypt/live/preview.xyz.abc/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/preview.xyz.abc/privkey.pem; + + # GZIP + gzip on; + gzip_disable "msie6"; + + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_min_length 256; + gzip_types text/xml text/javascript font/ttf font/eot font/otf application/rdf+xml application/x-javascript application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy; + + # Remove X-Powered-By, which is an information leak + fastcgi_hide_header X-Powered-By; + + # Do not send nginx server header + server_tokens off; + + add_header Access-Control-Allow-Origin *; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; + add_header Referrer-Policy "strict-origin" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header X-Content-Type-Options "nosniff" always; + + access_log on; + error_log off; + + root /var/www/preview/$sub; + + location / { + index index.html; + } +} +``` + +### Cleanup cron job +Delete previews with not activity in the last 30 days. +```bash +# /home/ubuntu/cronDeleteUnusedPreviews.sh + +# Define the directory to search in. Modify this variable to suit your needs. +SEARCH_DIR="/var/www/preview" + +# Find and delete directories not modified in the last 30 days. +find "$SEARCH_DIR" -type d -mtime +30 -exec rm -rf {} + + +# Explanation: +# - `find "$SEARCH_DIR"`: Start searching in the specified directory. +# - `-type d`: Only look for directories. +# - `-mtime +30`: Find directories that were last modified more than 30 days ago. +# - `-exec rm -rf {} +`: Delete each directory found ({} is replaced by the found directory name). +``` + +Crontab example: +```bash +@daily /home/ubuntu/cronDeleteUnusedPreviews.sh +``` + +## Release instructions + +In order to release a new version of this action: + +1. Locate the semantic version of the [upcoming release][release-list] (a draft is maintained by the [`draft-release` workflow][draft-release]). + +2. Publish the draft release from the `main` branch with semantic version as the tag name, _with_ the checkbox to publish to the GitHub Marketplace checked. :ballot_box_with_check: + +3. After publishing the release, the [`release` workflow][release] will automatically run to create/update the corresponding the major version tag such as `v0`. + + ⚠️ Environment approval is required. Check the [Release workflow run list][release-workflow-runs]. + +## License + +The scripts and documentation in this project are released under the [MIT License](LICENSE). + + +[release-list]: https://github.com/dafnik/ssp/releases +[draft-release]: .github/workflows/draft-release.yml +[release]: .github/workflows/release.yml +[release-workflow-runs]: https://github.com/dafnik/ssp/actions/workflows/release.yml diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..308c42c --- /dev/null +++ b/action.yml @@ -0,0 +1,103 @@ +name: 'Static Site Preview (SSP)' +description: 'Deploy static site previews to a self-hosted server via ssh' +author: "Dafnik" +branding: + icon: 'arrow-down-circle' + color: 'purple' + +inputs: + source: + required: true + description: 'Path to the files which should be deployed' + target: + required: true + description: 'Preview server target path, must be a directory path.' + host: + required: true + description: 'Preview server domain' + port: + description: 'Preview server ssh port' + default: '22' + username: + required: true + description: 'Preview server ssh username' + key: + description: 'Preview server ssh key content of private key. ex raw content of ~/.ssh/id_rsa' + strip_components: + description: 'remove the specified number of leading path elements' + default: '0' + delete_treshhold_days: + default: '30' + description: 'Number of days after inactive previews are deleted' + +runs: + using: 'composite' + steps: + - name: Check if run from pull request + shell: bash + if: github.event_name != 'pull_request' + run : | + echo "static-site-preview can only be run from pull request" + echo "Check for pull request like this" + echo "if: github.event_name == 'pull_request'" + exit 1 + + - id: setup + shell: bash + name: Setup hash identifier & date + run: | + echo "name=${{ github.event.repository.name }}-${{ github.event.number }}" >> "$GITHUB_OUTPUT" + current_date=$(date '+%Y-%m-%d %H:%M:%S (%Z)') + echo "current_date=$current_date" >> "$GITHUB_OUTPUT" + future_date=$(date -d "+${{ inputs.delete_treshhold_days }} days" '+%Y-%m-%d') + echo "future_date=$future_date" >> "$GITHUB_OUTPUT" + + - id: hash + shell: bash + name: Create hash + run: | + echo Hash identifier: "${{ steps.setup.outputs.name }}" + hash=$(echo -n "${{ steps.setup.outputs.name }}" | md5sum | awk '{print $1}') + short_hash=${hash:0:10} + echo "md5=$short_hash" >> "$GITHUB_OUTPUT" + echo "url=https://$short_hash.${{ inputs.host }}" >> "$GITHUB_OUTPUT" + + - name: Check if hash is empty + shell: bash + run: | + if [ -z "${{ steps.hash.outputs.md5 }}" ]; then + echo "MD5 hash is empty. Failing the workflow." + exit 1 + else + echo "MD5 hash: ${{ steps.hash.outputs.md5 }}" + fi + + - name: Copy source files to target server + uses: appleboy/scp-action@v0.1.7 + with: + overwrite: true + host: ${{ inputs.host }} + username: ${{ inputs.username }} + key: ${{ inputs.key }} + port: ${{ inputs.port }} + source: ${{ inputs.source }} + target: "${{ inputs.target }}/${{ steps.hash.outputs.md5 }}" + strip_components: ${{ inputs.strip_components }} + + - name: Create or update sticky pull request comment + uses: marocchino/sticky-pull-request-comment@v2 + with: + message: | + This pull request has been automatically deployed using Dafnik's static site preview (SSP) service. + [Learn more](https://github.com/Dafnik/ssp). + + 🔎 **Commit:** ${{ github.sha }} + 📅 **Updated at:** ${{ steps.setup.outputs.current_date }} + ✅ **Preview URL:** <${{ steps.hash.outputs.url }}> + + **Note:** If there is no further activity, this preview will be deleted **${{ inputs.delete_treshhold_days }} day(s) from now** on ${{ steps.setup.outputs.future_date }}. + +outputs: + url: + description: Deployment url + value: ${{ steps.hash.outputs.url }} diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..3519ba4 --- /dev/null +++ b/renovate.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "baseBranches": ["main"], + "enabledManagers": ["github-actions"], + "semanticCommits": "enabled", + "semanticCommitScope": "", + "semanticCommitType": "build", + "commitBody": "See associated pull request for more information.", + "schedule": [ + "after 11:00pm on the 1 day of the month", + "before 4am on the 2 day of the month", + "after 11:00pm on the 15 day of the month", + "before 4am on the 16 day of the month" + ], + "timezone": "Europe/Vienna", + "labels": ["dependencies"], + "assignees": ["Dafnik"], + "packageRules": [ + { + "matchPackagePatterns": ["*"], + "matchUpdateTypes": ["minor", "patch", "pin", "digest"], + "groupName": "minor-updates" + }, + { + "matchUpdateTypes": ["major"], + "groupName": "major-update" + } + ], + "extends": ["config:base", ":pinVersions", ":unpublishSafe", ":prNotPending", ":preserveSemverRanges", ":rebaseStalePrs"] +} \ No newline at end of file