diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba2c4cc..2cab376 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,14 @@ name: PHP Checks +concurrency: + group: php-checks-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + on: + # Pull requests validate topic branches; push runs are reserved for main to avoid + # duplicate CI runs for every commit on a branch that already has an open PR. push: - branches: [main, "feat/**", "fix/**", "test/**"] + branches: [main] pull_request: workflow_dispatch: @@ -12,6 +18,7 @@ permissions: jobs: checks: runs-on: ubuntu-latest + timeout-minutes: 20 services: mariadb: image: mariadb:10.11 @@ -31,7 +38,7 @@ jobs: php: ["8.2", "8.3", "8.4"] steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up PHP uses: shivammathur/setup-php@v2 @@ -52,10 +59,16 @@ jobs: - name: Check design-system assets run: php tests/design-system.php + - name: Check workspace UI contracts + run: php tests/workspace-ui.php + + - name: Check deployment workflow safety + run: php tests/deployment-workflow.php + - name: Check JavaScript syntax run: | node --check public/assets/js/ui.js - node --check public/assets/js/design-system-demo.js + node --check public/assets/js/workspace.js - name: Run MariaDB integration tests env: diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..29e869b --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,106 @@ +name: Deploy production + +on: + workflow_dispatch: + inputs: + confirm_production: + description: Type DEPLOY to confirm a production deployment + required: true + type: string + +permissions: + contents: read + +concurrency: + group: production-deployment + cancel-in-progress: false + +jobs: + deploy: + if: ${{ inputs.confirm_production == 'DEPLOY' && github.ref == 'refs/heads/main' }} + runs-on: ubuntu-latest + environment: production + timeout-minutes: 15 + steps: + - name: Check out the selected revision + uses: actions/checkout@v5 + + - name: Validate deployment configuration + shell: bash + env: + DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} + DEPLOY_USER: ${{ secrets.DEPLOY_USER }} + DEPLOY_SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }} + DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }} + DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }} + run: | + set -euo pipefail + for name in DEPLOY_HOST DEPLOY_USER DEPLOY_SSH_PRIVATE_KEY DEPLOY_KNOWN_HOSTS DEPLOY_PATH; do + if [[ -z "${!name}" ]]; then + echo "::error::Required production secret is missing: ${name}" + exit 1 + fi + done + if [[ ! "$DEPLOY_PATH" =~ ^/[A-Za-z0-9._/-]+$ ]]; then + echo "::error::DEPLOY_PATH must be an absolute path using only letters, numbers, dot, underscore, slash, or hyphen" + exit 1 + fi + if [[ "$DEPLOY_PATH" == *"/../"* || "$DEPLOY_PATH" == */.. || "$DEPLOY_PATH" == "/" || "$DEPLOY_PATH" == "/home" || "$DEPLOY_PATH" == "/var" ]]; then + echo "::error::DEPLOY_PATH is too broad to be a safe application target" + exit 1 + fi + + - name: Configure pinned SSH identity + shell: bash + env: + DEPLOY_SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }} + DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }} + run: | + set -euo pipefail + install -m 700 -d "$HOME/.ssh" + printf '%s\n' "$DEPLOY_SSH_PRIVATE_KEY" > "$HOME/.ssh/deploy_key" + chmod 600 "$HOME/.ssh/deploy_key" + printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > "$HOME/.ssh/known_hosts" + chmod 600 "$HOME/.ssh/known_hosts" + + - name: Validate remote SSH access and target directory + shell: bash + env: + DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} + DEPLOY_USER: ${{ secrets.DEPLOY_USER }} + DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }} + DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }} + run: | + set -euo pipefail + port="${DEPLOY_PORT:-22}" + [[ "$port" =~ ^[0-9]+$ ]] || { echo "::error::DEPLOY_PORT must be numeric"; exit 1; } + ssh -i "$HOME/.ssh/deploy_key" -p "$port" \ + -o BatchMode=yes -o StrictHostKeyChecking=yes \ + "$DEPLOY_USER@$DEPLOY_HOST" \ + "test -d '$DEPLOY_PATH' && test -w '$DEPLOY_PATH'" + + - name: Deploy application files without deleting remote data + shell: bash + env: + DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} + DEPLOY_USER: ${{ secrets.DEPLOY_USER }} + DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }} + DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }} + run: | + set -euo pipefail + port="${DEPLOY_PORT:-22}" + rsync -az --checksum \ + --exclude='/.git/' \ + --exclude='/.github/' \ + --exclude='/tests/' \ + --exclude='/docs/' \ + --exclude='/.env' \ + --exclude='/.env.*' \ + --exclude='/*.log' \ + -e "ssh -i $HOME/.ssh/deploy_key -p $port -o BatchMode=yes -o StrictHostKeyChecking=yes" \ + ./ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/" + + - name: Remove temporary SSH credentials + if: always() + shell: bash + run: rm -f "$HOME/.ssh/deploy_key" "$HOME/.ssh/known_hosts" diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..e2f816e --- /dev/null +++ b/.htaccess @@ -0,0 +1,30 @@ +Options -Indexes + + + RewriteEngine On + + # Keep repository metadata and secret dotfiles inaccessible. + RewriteRule (^|/)\.(?!well-known/) - [F,L] + + # An optional repository-root document root must never expose the source tree. + # Only the public assets are mapped directly; every application URL goes through PHP. + RewriteRule ^public(?:/|$) - [F,L,NC] + RewriteRule ^\.well-known/ - [END] + RewriteRule ^assets/(.*)$ public/assets/$1 [END,NC] + RewriteRule ^ public/index.php [QSA,END] + + + + AddType text/css .css + AddType application/javascript .js + + + + + Header set Cache-Control "public, max-age=31536000, immutable" + + Header always set X-Content-Type-Options "nosniff" + Header always set Referrer-Policy "strict-origin-when-cross-origin" + Header always set X-Frame-Options "DENY" + Header always set Content-Security-Policy "default-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; object-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'" + diff --git a/CHANGELOG.md b/CHANGELOG.md index 8313304..a36e60d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ ## Unreleased +- Add a manual, main-only production delivery workflow with typed confirmation, production environment gate, pinned SSH host verification, destination validation, and non-destructive rsync. +- Add CI contract tests for deployment safety and document required production secrets and controlled migration steps. +- Reduce redundant CI traffic by running topic-branch validation through pull requests and reserving push-triggered CI for main. +- Fix a workspace UI contract test to match idempotent `CREATE TABLE IF NOT EXISTS` migration statements. + +- Fix CSS/JavaScript delivery for hosts whose document root is the repository root by routing `/assets/*` to the public asset directory without exposing application source files. +- Explicitly declare static CSS/JavaScript MIME types and apply immutable caching to versioned assets; bump workspace asset keys to avoid stale cached styles and scripts. +- Run the previously unwired workspace UI contract test in CI and syntax-check the production `workspace.js` controller. +- Remove the standalone visual showcase page and demo-only assets; retain shared design-system CSS that the live workspace uses. + + - Removed unused duplicate team/project settings handlers and aligned UI contract tests with the registered transactional update endpoints. - Add a project Files tab with a searchable private source-file directory, lightweight text editor, save/reload controls, and revision-history restore. diff --git a/README.md b/README.md index 99f907b..3b16624 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # TeamCode Hub -**Version:** 0.2.0-dev +**Version:** 0.2.2-dev **Status:** Active development TeamCode Hub is a modular, security-focused collaboration platform for software teams, designed for conventional PHP hosting with cPanel/DirectAdmin and MySQL/MariaDB. @@ -63,7 +63,7 @@ See [docs/API.md](docs/API.md) for request bodies, status codes, retention, and ## Validation -CI checks PHP syntax and smoke tests, design-system and workspace UI contracts, JavaScript syntax, and MariaDB integration tests on PHP 8.2, 8.3, and 8.4. +CI checks PHP syntax and smoke tests, design-system and workspace UI contracts, deployment safety contracts, JavaScript syntax, and MariaDB integration tests on PHP 8.2, 8.3, and 8.4. Production delivery is manual-only from `main`, requires a typed confirmation and pinned SSH host key, and must be configured with environment-scoped deployment secrets; it never deletes remote files or runs migrations automatically. Run the lightweight local checks with: @@ -71,8 +71,8 @@ Run the lightweight local checks with: php tests/run.php php tests/design-system.php php tests/workspace-ui.php +php tests/deployment-workflow.php node --check public/assets/js/ui.js -node --check public/assets/js/design-system-demo.js node --check public/assets/js/workspace.js ``` diff --git a/docs/DESIGN-SYSTEM.md b/docs/DESIGN-SYSTEM.md index 5039cea..6046437 100644 --- a/docs/DESIGN-SYSTEM.md +++ b/docs/DESIGN-SYSTEM.md @@ -11,20 +11,20 @@ This document describes the shared visual and interaction foundation used by Tea | `public/assets/css/base.css` | Browser defaults, semantic element defaults, focus visibility, and reduced motion | | `public/assets/css/layout.css` | Containers, responsive grids, page shell, sidebar, and header layouts | | `public/assets/css/components.css` | Buttons, forms, badges, cards, alerts, tables, tabs, overlays, and feedback | -| `public/assets/css/patterns.css` | Upload/range controls, avatar states, pagination, accordions, tooltips, workflow steps, quote/code, and footer patterns | +| `public/assets/css/patterns.css` | Reusable upload/range controls, avatar states, pagination, accordions, tooltips, workflow steps, and content patterns | | `public/assets/css/utilities.css` | Small composable spacing, text, surface, and alignment helpers | -| `public/assets/js/ui.js` | Accessible interactions shared by product pages |\n| `public/assets/css/workspace.css` | Product-specific workspace, dashboard, project, task, and chat presentation |\n| `public/assets/js/workspace.js` | Authenticated API integration and workspace interactions |\n| `public/app.html` | English workspace shell and sign-in form | -| `public/assets/css/design-system-demo.css` | Styles unique to the reference showcase | -| `public/assets/js/design-system-demo.js` | Demo-only button handlers | -| `public/design-system.html` | English component reference page | +| `public/assets/css/workspace.css` | Product-specific workspace, dashboard, team, project, task, and chat presentation | +| `public/assets/js/ui.js` | Accessible interactions shared by product pages | +| `public/assets/js/workspace.js` | Authenticated API integration and workspace interactions | +| `public/app.html` | English workspace shell and sign-in form | ## Loading the design system Load the entry stylesheet and the shared UI controller from a page template. Keep product-specific styles and behavior in separate files and avoid inline style or script blocks. ```html - - + + ``` The entry stylesheet imports tokens, base rules, layout, core components, extended patterns, and utilities in dependency order. Every imported stylesheet URL carries a cache key. When CSS changes, bump the CSS asset query keys in the entry imports and HTML entry points together. A production release should use its actual SemVer version. @@ -85,14 +85,14 @@ Run: ```sh php tests/design-system.php node --check public/assets/js/ui.js -node --check public/assets/js/design-system-demo.js +node --check public/assets/js/workspace.js ``` CI runs these checks in addition to PHP lint, smoke tests, and MariaDB integration tests. These checks do not replace manual browser review, assistive-technology testing, or visual regression testing. ## Current scope -The showcase is a reference implementation of the style system, not a claim that the product dashboard, team/project screens, or chat interface are complete. Product screens must be built on the existing APIs and retain server-side authentication and authorization checks. +The shared style layers are loaded by the actual workspace shell; they are not a standalone product or a substitute for connected application behavior. Team, project, task, file, and chat actions must use the existing APIs and retain server-side authentication and authorization checks. ## Extended component coverage diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 314fa66..d2e8de2 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -3,6 +3,7 @@ ## Before deployment - Configure the document root to the repository's `public/` directory where possible. +- If the host cannot point its document root at `public/`, the repository-root `.htaccess` provides a locked-down fallback: it exposes only `/assets/*` from `public/assets/` and routes app paths through `public/index.php`. Do not disable the root rewrite rules, because they prevent direct access to repository source files. - Configure database environment variables through the host or a private configuration mechanism outside the web root. - Create a dedicated database user with only the permissions the application requires. - Enable HTTPS and configure session cookie security accordingly. If TLS terminates at a trusted reverse proxy, set `APP_HTTPS=true` only when the deployment topology guarantees HTTPS to the browser. @@ -10,6 +11,19 @@ - Confirm private storage, configuration, backup, and log paths cannot be downloaded over HTTP. - Back up the database and files before applying schema changes. +## GitHub Actions deployment (manual) + +The `.github/workflows/deploy.yml` workflow is a manual SSH-based delivery path. It only permits the `main` branch, requires typing `DEPLOY`, uses the GitHub `production` environment, verifies a pinned SSH host key, checks that the target directory already exists and is writable, and does not delete remote files. Configure environment-scoped secrets before using it: + +- `DEPLOY_HOST` — server hostname (no protocol or path). +- `DEPLOY_USER` — dedicated deployment account with access only to the application directory. +- `DEPLOY_SSH_PRIVATE_KEY` — private key for that account; never commit it. +- `DEPLOY_KNOWN_HOSTS` — independently verified SSH host-key line(s), not output blindly trusted from a runtime `ssh-keyscan`. +- `DEPLOY_PATH` — absolute application-root path containing the `app/` and `public/` directories; it must not be the filesystem root or a broad system directory. +- `DEPLOY_PORT` — optional SSH port; defaults to `22`. For a non-standard port, the pinned `known_hosts` entry must use the matching `[host]:port` format. + +Set production environment reviewers and branch restrictions in GitHub repository settings. Point the web server document root to the deployed `public/` directory when possible. The workflow deliberately does not run database migrations or create an administrator automatically: take a backup, review pending migrations, and apply `php bin/migrate.php` from a trusted server-side CLI session as a separate controlled operation. Configure the required application environment variables on the server before enabling traffic. The first deployment must be tested on a staging target before production use. + ## Registration and email verification Public registration is enabled after the account-token migration has been applied. Configure these environment variables outside the web root: diff --git a/docs/WORKSPACE-UI.md b/docs/WORKSPACE-UI.md index 7ab1a7a..3d2a351 100644 --- a/docs/WORKSPACE-UI.md +++ b/docs/WORKSPACE-UI.md @@ -7,13 +7,14 @@ This document maps the English browser application to the existing server APIs. - `/` — authenticated workspace shell; shows sign-in when there is no active session. - `/login` — the same progressive-enhancement shell, initially displaying sign-in when unauthenticated. - `/verify-email` — the workspace shell used to confirm a one-time token supplied in the URL fragment. -- `/design-system.html` — component reference page. - `public/app.html` — semantic workspace shell. - `public/assets/css/app.css` and its token/base/layout/component/pattern/utility layers — shared design-system styles. - `public/assets/css/workspace.css` — product-only styles layered on the core design system. - `public/assets/js/workspace.js` — same-origin API client, navigation, rendering, forms, and chat polling. - `public/assets/js/ui.js` — shared design-system interactions. +The preferred deployment sets the document root to `public/`. A repository-root document root is also supported by the top-level `.htaccess`, which maps `/assets/*` to `public/assets/*`, routes application paths to `public/index.php`, and blocks direct access to the `public/` prefix and repository files. + Project repository links accept only public HTTPS GitHub URLs. The server shares one daily cached snapshot per repository and caps refresh attempts to 20 distinct repositories per UTC day. The HTML has no inline style or script blocks. The front controller returns HTML with a restrictive same-origin Content Security Policy. CSS and JavaScript are external, versioned files; update their query-string version when releasing a changed asset set. @@ -103,4 +104,4 @@ php tests/workspace-ui.php node --check public/assets/js/workspace.js ``` -These checks validate the static asset contract and JavaScript syntax. They do not replace manual browser testing across supported devices, keyboard-only interaction, screen-reader review, or visual regression tests. The design-system showcase includes extended input, avatar, chip, accordion, workflow-step, pagination, and content patterns. +These checks validate the static asset contract and JavaScript syntax. They do not replace manual browser testing across supported devices, keyboard-only interaction, screen-reader review, or visual regression tests. The style contract tests check actual production assets and route wiring; reusable patterns stay in the shared `patterns.css` layer instead of a separate demo page. diff --git a/public/.htaccess b/public/.htaccess index 26ca016..1f2475e 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -13,7 +13,16 @@ DirectoryIndex index.php RewriteRule ^ index.php [QSA,L] + + # Keep strict nosniff policies compatible with directly served static assets. + AddType text/css .css + AddType application/javascript .js + + + + Header set Cache-Control "public, max-age=31536000, immutable" + Header always set X-Content-Type-Options "nosniff" Header always set Referrer-Policy "strict-origin-when-cross-origin" Header always set X-Frame-Options "DENY" diff --git a/public/app.html b/public/app.html index aea8d23..66f0c3a 100644 --- a/public/app.html +++ b/public/app.html @@ -6,10 +6,10 @@ TeamCode Hub — Workspace - - - - + + + + @@ -171,11 +171,11 @@

Verify your email

-
+
+ Your teamsLoading teams… + +