diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..eb4eb31 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,37 @@ +# Weekly dependency update pull requests into development, grouped to keep them few. +# Security updates are separate: Dependabot opens those against main as soon as an advisory appears. +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + target-branch: development + schedule: + interval: weekly + groups: + github-actions: + patterns: + - '*' + + - package-ecosystem: composer + directory: / + target-branch: development + schedule: + interval: weekly + open-pull-requests-limit: 5 + groups: + composer-minor-and-patch: + update-types: + - minor + - patch + + - package-ecosystem: npm + directory: / + target-branch: development + schedule: + interval: weekly + open-pull-requests-limit: 5 + groups: + javascript-minor-and-patch: + update-types: + - minor + - patch diff --git a/.github/known_hosts b/.github/known_hosts new file mode 100644 index 0000000..69ac1bf --- /dev/null +++ b/.github/known_hosts @@ -0,0 +1,8 @@ +# SSH host keys for the deploy servers (production 3.142.169.134, development 3.138.217.206). +# Regenerate with: ssh-keyscan -t ed25519,ecdsa,rsa , and check the fingerprints first. +3.138.217.206 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEJl6SHNoIy2OY2LOcELcUdPaNPbB/bmgQ7IbjRIO1L60NAf3hdvFY/FAfSsOpSB+z8nrdW6nwwWFzuFXcTH1Z4= +3.138.217.206 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPiwbRfFX9y1YWZnShLEdUj2ITY2F84XqOEVGTldAUNT +3.138.217.206 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC/6L95wPTNeegZmuwHg+494aIJqv7rMKpMVWMNJATel9+if0Jn3VMxCQHDeadTKAsIs6d6J3dGnixBGBWJHN0oWkgIc4vHhemt0ROGX1kMtkeyyQfowNb6JcKemrMItsQGK4PgtSTVghr5emFDex9T1cYN8+T1yDy9zM2h2vyldq3tI8GbFzdvP47VeqcsELTKmaRNttTTNFlZeo/MQfL20MkM69ggZVmreEHjcYLbqsKpqZqttynHIbLATJeYwCyY+dr20fpOcNq7FE4ctsOtfFjyVt/vczZw+W7Bg7Z5AHyyAI6oIYqrBO1R3+A9q9dS+L6SVyuOBr2MgHjk4VL9bTbjCNMTbIGpg2frR3z/oQ73eC3HIAfeRrWuyvEg5E+kRjumJbNQXmtVGp1Lxqxd3oD/3dgv3oRsqdnXrCUmnU5mfAo0iIN0yZYG4A4COHSnZIMFMtCq8KOd42QUK/GmwrvSzVmb5jZJJBPeMiJM4HRsPjfknZdBDmhgplzk/5s= +3.142.169.134 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEGPgYtcd5CBDJncrj/7KWrWPC91kZLz5SqgTQ8zjrcMrNrnmPrzwWt8OAwxcpb77AJ7feu1OH6/udSqDi+/bS0= +3.142.169.134 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPli28DWaiaDUrAf/gIfi9HSC10/CZl7C2e0EsgOuqsh +3.142.169.134 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCZrRSWwNrkHZSJw1ssb/RfYP79CdF5qQUPZZiGD3FchlxK9wzwPgkJVoDYwVchlQZLPyhBAw/UGIa6O14xslevl1sdoKSIly2tQX024pzUnK7rndm9/2qtcBwLB5mtJeGThQbyDS5jp6stLHMho3vUGoKc5TyLJyBwUZD0/M5w8+cejNr7qyFltaOv6tgT2w3sqO0K4WMA9qoAh7wEe5Ye4x8qd8dWpu/8qL1VY2ukrJa/wXIA7LE5cpdiXA1hawYmqki4OuyNJTdkgLd9oyXaN4XEkL+hLIaQjrlVvpTBkW5zmGtuUupEVdrVkrd70vRr9iuJg5Mtq2glq31gpgb2WIXbnIFtgiAG+6KCbfLMMg8hQC2EnucAf5k4Vq9H3DUu1ByHIxqPAnIHNkjvS6YHg9Xnp6nZKAoiGNi1UZNX30gvIj2rS3zt/vFJUdvORXSVR+eW096EgK0aXRfbe5eVLscc7ZI5IxgxMTULosnPInwalPlszOgnxoryMh1ZOx8= diff --git a/.github/workflows/deploy-environment.yml b/.github/workflows/deploy-environment.yml new file mode 100644 index 0000000..2b81250 --- /dev/null +++ b/.github/workflows/deploy-environment.yml @@ -0,0 +1,141 @@ +# Build and deploy one environment +# +# Called by deploy.yml for production and development. It builds the assets, +# uploads the deployment package as an artifact, and runs Deployer, which +# downloads that artifact on the server (deploy:ci-artifacts). + +name: Deploy environment + +on: + workflow_call: + inputs: + environment: + description: 'GitHub environment and Deployer host: production or development' + required: true + type: string + +permissions: + id-token: write + contents: read + +jobs: + deploy: + runs-on: ubuntu-latest + environment: ${{ inputs.environment }} + steps: + - uses: actions/checkout@v6 + + - name: Get Runner IP + id: ip + run: echo "ipv4=$(curl -s https://checkip.amazonaws.com)" >> "$GITHUB_OUTPUT" + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v6 + with: + role-to-assume: arn:aws:iam::147899039648:role/GitHubActionsDeployRole + aws-region: ${{ secrets.AWS_REGION }} + + - name: Whitelist Runner IP in AWS Security Group + run: | + aws ec2 authorize-security-group-ingress \ + --region ${{ secrets.AWS_REGION }} \ + --group-id ${{ secrets.AWS_SECURITY_GROUP_ID }} \ + --protocol tcp \ + --port 22 \ + --cidr ${{ steps.ip.outputs.ipv4 }}/32 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.5' + extensions: mbstring, xml, bcmath, ctype, json, tokenizer, pdo, pdo_mysql + coverage: none + + - name: Get Composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT" + + - name: Cache Composer downloads + uses: actions/cache@v5 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Create storage directories + run: | + mkdir -p storage/framework/cache/data + mkdir -p storage/framework/sessions + mkdir -p storage/framework/views + mkdir -p bootstrap/cache + + - name: Install Composer dependencies for deployment runner + run: composer install --prefer-dist --no-progress --no-interaction --optimize-autoloader --no-scripts + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '20' + cache: 'yarn' + + - name: Install Yarn dependencies + run: yarn install --frozen-lockfile --ignore-engines + + - name: Build assets + run: npm run production + + - name: Create deployment package + run: | + rm -rf deployment-package || true + TEMP_DIR=$(mktemp -d) + rsync -av \ + --exclude=node_modules \ + --exclude=.git \ + --exclude=.github \ + --exclude=tests \ + --exclude=storage/logs \ + --exclude=vendor \ + . "$TEMP_DIR/" + cp -r public/css public/js public/fonts public/images public/svg public/mix-manifest.json "$TEMP_DIR/public/" || true + mv "$TEMP_DIR" deployment-package + + - name: Upload deployment artifact + uses: actions/upload-artifact@v7 + with: + name: digitizationacademy-${{ github.sha }} + path: deployment-package/ + retention-days: 30 + + - name: Setup SSH key + uses: webfactory/ssh-agent@v0.10.0 + with: + ssh-private-key: ${{ secrets.DEPLOY_PRIVATE_KEY }} + + - name: Trust the deploy servers' SSH host keys + run: | + mkdir -p ~/.ssh + cat .github/known_hosts >> ~/.ssh/known_hosts # append; ssh-agent may already have added entries + cat > ~/.ssh/config <<'EOF' + Host * + StrictHostKeyChecking yes + EOF + chmod 700 ~/.ssh + chmod 600 ~/.ssh/config ~/.ssh/known_hosts + + - name: Deploy with Deployer + run: vendor/bin/dep deploy ${{ inputs.environment }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_SHA: ${{ github.sha }} + GITHUB_REPO: ${{ github.repository }} + + - name: Revoke Runner IP from AWS Security Group + if: always() + run: | + aws ec2 revoke-security-group-ingress \ + --region ${{ secrets.AWS_REGION }} \ + --group-id ${{ secrets.AWS_SECURITY_GROUP_ID }} \ + --protocol tcp \ + --port 22 \ + --cidr ${{ steps.ip.outputs.ipv4 }}/32 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 68edbfa..207137b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,8 +1,9 @@ # CI/CD Deployment Workflow for Digitization Academy # # WORKFLOW: -# - Manual run on 'development' branch → Deploys to DEVELOPMENT -# - Push to 'main' branch → Calculates version → Deploys to PRODUCTION → Creates GitHub release (only on success) +# - Manual run on 'development' branch → Tests → Deploys to DEVELOPMENT +# - Push to 'main' branch → Tests → Calculates version → Deploys to PRODUCTION → Creates GitHub release (only on success) +# - Pull requests run the tests on their own (tests.yml) # # VERSION CONTROL: # Use commit message tags to control version bumps: @@ -26,8 +27,19 @@ permissions: id-token: write contents: read +# One run at a time per branch, so two quick merges can't deploy at once or +# calculate the same version. A queued run waits; it is never cancelled mid-deploy. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + jobs: + tests: + if: "github.event_name == 'workflow_dispatch' || !contains(github.event.head_commit.message, '[skip deploy]') && !contains(github.event.head_commit.message, '[no deploy]')" + uses: ./.github/workflows/tests.yml + calculate-version: + needs: tests runs-on: ubuntu-latest if: "github.event_name == 'push' && github.ref == 'refs/heads/main' && !contains(github.event.head_commit.message, '[skip deploy]') && !contains(github.event.head_commit.message, '[no deploy]')" outputs: @@ -43,36 +55,33 @@ jobs: env: CURRENT_COMMIT_MSG: ${{ github.event.head_commit.message }} run: | - # Get the latest tag, check if any tags exist - LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") - - if [ -z "$LATEST_TAG" ]; then - echo "No tags found - this is the first release" - LATEST_TAG="0.0.0" - COMMITS=$(git log --oneline) - echo "All commits (first release):" + # Select the highest semantic-version tag reachable from HEAD. + LATEST_TAG="$(git tag --merged HEAD --list --sort=-version:refname | grep -m1 -E '^[0-9]+\.[0-9]+\.[0-9]+$' || true)" + LATEST_TAG="${LATEST_TAG:-0.0.0}" + echo "Latest tag: $LATEST_TAG" + + echo "Commits since last tag:" + if [ "$LATEST_TAG" = "0.0.0" ]; then + git log --oneline else - echo "Latest tag: $LATEST_TAG" - COMMITS=$(git log ${LATEST_TAG}..HEAD --oneline) - echo "Commits since last tag:" + git log "${LATEST_TAG}..HEAD" --oneline fi - echo "$COMMITS" - - echo "Current commit message: $(printf '%s' "$CURRENT_COMMIT_MSG")" + echo "Current commit message:" + printf '%s\n' "$CURRENT_COMMIT_MSG" # Determine version bump based on the current commit message only - if echo "$CURRENT_COMMIT_MSG" | grep -i "\[major\]\|\[breaking\]"; then + if printf '%s' "$CURRENT_COMMIT_MSG" | grep -Eqi '\[(major|breaking)\]'; then BUMP_TYPE="major" - elif echo "$CURRENT_COMMIT_MSG" | grep -i "\[minor\]\|\[feature\]"; then + elif printf '%s' "$CURRENT_COMMIT_MSG" | grep -Eqi '\[(minor|feature)\]'; then BUMP_TYPE="minor" else BUMP_TYPE="patch" fi echo "Bump type: $BUMP_TYPE" - echo "bump_type=$BUMP_TYPE" >> $GITHUB_OUTPUT - echo "current_version=$LATEST_TAG" >> $GITHUB_OUTPUT + echo "bump_type=$BUMP_TYPE" >> "$GITHUB_OUTPUT" + echo "current_version=$LATEST_TAG" >> "$GITHUB_OUTPUT" - name: Calculate new version id: new_version @@ -113,273 +122,50 @@ jobs: fi echo "New version: $NEW_VERSION" - echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT + echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT" build-and-deploy-production: needs: calculate-version - runs-on: ubuntu-latest - environment: production - if: "github.event_name == 'push' && github.ref == 'refs/heads/main' && !contains(github.event.head_commit.message, '[skip deploy]') && !contains(github.event.head_commit.message, '[no deploy]')" - steps: - - uses: actions/checkout@v6 - - - name: Get Runner IP - id: ip - run: echo "ipv4=$(curl -s https://checkip.amazonaws.com)" >> "$GITHUB_OUTPUT" - - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v6 - with: - role-to-assume: arn:aws:iam::147899039648:role/GitHubActionsDeployRole - aws-region: ${{ secrets.AWS_REGION }} - - - name: Whitelist Runner IP in AWS Security Group - run: | - aws ec2 authorize-security-group-ingress \ - --region ${{ secrets.AWS_REGION }} \ - --group-id ${{ secrets.AWS_SECURITY_GROUP_ID }} \ - --protocol tcp \ - --port 22 \ - --cidr ${{ steps.ip.outputs.ipv4 }}/32 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.5' - extensions: mbstring, xml, bcmath, ctype, json, tokenizer, pdo, pdo_mysql - - - name: Cache Composer packages - id: composer-cache - uses: actions/cache@v5 - with: - path: vendor - key: ${{ runner.os }}-php-v3-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-php-v3- - - - name: Create storage directories - run: | - mkdir -p storage/framework/cache/data - mkdir -p storage/framework/sessions - mkdir -p storage/framework/views - mkdir -p bootstrap/cache - - - name: Install Composer dependencies for deployment runner - run: composer install --prefer-dist --no-progress --optimize-autoloader --no-scripts - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '20' - cache: 'yarn' - - - name: Install Yarn dependencies - run: yarn install --frozen-lockfile --ignore-engines - - - name: Build assets for Production - run: npm run production - - - name: Create deployment package - run: | - rm -rf deployment-package || true - TEMP_DIR=$(mktemp -d) - rsync -av \ - --exclude=node_modules \ - --exclude=.git \ - --exclude=.github \ - --exclude=tests \ - --exclude=storage/logs \ - --exclude=vendor \ - . "$TEMP_DIR/" - cp -r public/css public/js public/fonts public/images public/svg public/mix-manifest.json "$TEMP_DIR/public/" || true - mv "$TEMP_DIR" deployment-package - - - name: Upload deployment artifact - uses: actions/upload-artifact@v7 - with: - name: digitizationacademy-${{ github.sha }} - path: deployment-package/ - retention-days: 30 - - - name: Setup SSH key - uses: webfactory/ssh-agent@v0.10.0 - with: - ssh-private-key: ${{ secrets.DEPLOY_PRIVATE_KEY }} - - - name: Add production host key - run: | - mkdir -p ~/.ssh - ssh-keyscan -H 3.142.169.134 >> ~/.ssh/known_hosts - - - name: Deploy with Deployer - run: vendor/bin/dep deploy production - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_SHA: ${{ github.sha }} - GITHUB_REPO: ${{ github.repository }} - - - name: Revoke Runner IP from AWS Security Group - if: always() - run: | - aws ec2 revoke-security-group-ingress \ - --region ${{ secrets.AWS_REGION }} \ - --group-id ${{ secrets.AWS_SECURITY_GROUP_ID }} \ - --protocol tcp \ - --port 22 \ - --cidr ${{ steps.ip.outputs.ipv4 }}/32 + uses: ./.github/workflows/deploy-environment.yml + with: + environment: production + secrets: inherit create-release: needs: [calculate-version, build-and-deploy-production] runs-on: ubuntu-latest permissions: contents: write - if: "github.event_name == 'push' && github.ref == 'refs/heads/main' && !contains(github.event.head_commit.message, '[skip deploy]') && !contains(github.event.head_commit.message, '[no deploy]')" steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: Create Release run: | - if [ "${{ needs.calculate-version.outputs.current_version }}" = "0.0.0" ]; then - RELEASE_NOTES="## What's Changed - - 🎉 **First Release!** - - Auto-generated initial release from main branch. - - **Latest commit:** - \`\`\` - $COMMIT_MESSAGE - \`\`\`" - else - RELEASE_NOTES="## What's Changed - - Auto-generated release from main branch. + VERSION="${{ needs.calculate-version.outputs.new_version }}" + PREVIOUS="${{ needs.calculate-version.outputs.current_version }}" - **Commits included:** - \`\`\` - $COMMIT_MESSAGE - \`\`\` + if gh release view "$VERSION" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + echo "Release $VERSION already exists; skipping creation." + exit 0 + fi - **Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ needs.calculate-version.outputs.current_version }}...${{ needs.calculate-version.outputs.new_version }}" + # Notes list every pull request merged since the previous release. + START_TAG=() + if gh release view "$PREVIOUS" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + START_TAG=(--notes-start-tag "$PREVIOUS") fi - gh release create "${{ needs.calculate-version.outputs.new_version }}" \ - --title "Release ${{ needs.calculate-version.outputs.new_version }}" \ - --notes "$RELEASE_NOTES" + gh release create "$VERSION" \ + --repo "$GITHUB_REPOSITORY" \ + --target "$GITHUB_SHA" \ + --title "Release $VERSION" \ + --generate-notes \ + "${START_TAG[@]}" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COMMIT_MESSAGE: ${{ github.event.head_commit.message }} build-and-deploy-development: - runs-on: ubuntu-latest - environment: development + needs: tests if: "github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/development'" - steps: - - uses: actions/checkout@v6 - - - name: Get Runner IP - id: ip - run: echo "ipv4=$(curl -s https://checkip.amazonaws.com)" >> "$GITHUB_OUTPUT" - - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v6 - with: - role-to-assume: arn:aws:iam::147899039648:role/GitHubActionsDeployRole - aws-region: ${{ secrets.AWS_REGION }} - - - name: Whitelist Runner IP in AWS Security Group - run: | - aws ec2 authorize-security-group-ingress \ - --region ${{ secrets.AWS_REGION }} \ - --group-id ${{ secrets.AWS_SECURITY_GROUP_ID }} \ - --protocol tcp \ - --port 22 \ - --cidr ${{ steps.ip.outputs.ipv4 }}/32 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.5' - extensions: mbstring, xml, bcmath, ctype, json, tokenizer, pdo, pdo_mysql - - - name: Cache Composer packages - id: composer-cache - uses: actions/cache@v5 - with: - path: vendor - key: ${{ runner.os }}-php-v3-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-php-v3- - - - name: Create storage directories - run: | - mkdir -p storage/framework/cache/data - mkdir -p storage/framework/sessions - mkdir -p storage/framework/views - mkdir -p bootstrap/cache - - - name: Install Composer dependencies for deployment runner - run: composer install --prefer-dist --no-progress --optimize-autoloader --no-scripts - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '20' - cache: 'yarn' - - - name: Install Yarn dependencies - run: yarn install --frozen-lockfile --ignore-engines - - - name: Build assets for Development - run: npm run production - - - name: Create deployment package - run: | - rm -rf deployment-package || true - TEMP_DIR=$(mktemp -d) - rsync -av \ - --exclude=node_modules \ - --exclude=.git \ - --exclude=.github \ - --exclude=tests \ - --exclude=storage/logs \ - --exclude=vendor \ - . "$TEMP_DIR/" - cp -r public/css public/js public/fonts public/images public/svg public/mix-manifest.json "$TEMP_DIR/public/" || true - mv "$TEMP_DIR" deployment-package - - - name: Upload deployment artifact - uses: actions/upload-artifact@v7 - with: - name: digitizationacademy-${{ github.sha }} - path: deployment-package/ - retention-days: 30 - - - name: Setup SSH key - uses: webfactory/ssh-agent@v0.10.0 - with: - ssh-private-key: ${{ secrets.DEPLOY_PRIVATE_KEY }} - - - name: Add development host key - run: | - mkdir -p ~/.ssh - ssh-keyscan -H 3.138.217.206 >> ~/.ssh/known_hosts - - - name: Deploy with Deployer - run: vendor/bin/dep deploy development - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_SHA: ${{ github.sha }} - GITHUB_REPO: ${{ github.repository }} - - - name: Revoke Runner IP from AWS Security Group - if: always() - run: | - aws ec2 revoke-security-group-ingress \ - --region ${{ secrets.AWS_REGION }} \ - --group-id ${{ secrets.AWS_SECURITY_GROUP_ID }} \ - --protocol tcp \ - --port 22 \ - --cidr ${{ steps.ip.outputs.ipv4 }}/32 \ No newline at end of file + uses: ./.github/workflows/deploy-environment.yml + with: + environment: development + secrets: inherit diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..5107898 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,83 @@ +# Tests and checks +# +# Runs on every pull request into main or development, and is called by +# deploy.yml before each deploy, so a failing test stops the deploy. +# +# - test: Pest (SQLite in memory, from phpunit.xml) and, on pull requests, Pint on the changed files. +# - audit: composer audit, on pull requests only. It reports vulnerable packages +# but never blocks a deploy. + +name: Tests + +on: + pull_request: + branches: + - main + - development + workflow_call: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 # Pint --diff compares against the base branch + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.5' + extensions: mbstring, xml, bcmath, ctype, json, tokenizer, curl, dom, fileinfo, intl, gd, pdo, pdo_sqlite, sqlite3, redis + coverage: none + + - name: Get Composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT" + + - name: Cache Composer downloads + uses: actions/cache@v5 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Install Composer dependencies + run: composer install --prefer-dist --no-progress --no-interaction --no-scripts + + - name: Prepare the application + run: | + # storage/framework isn't in git, and Blade needs its views directory. + mkdir -p storage/framework/cache/data storage/framework/sessions storage/framework/views bootstrap/cache + # phpunit.xml provides the test settings. The app key is generated for each run, + # so no key is committed (locally, .env provides one). + echo 'APP_KEY=' > .env + php artisan key:generate --force --ansi + php artisan package:discover --ansi + + - name: Check code style of changed files (Pint) + if: github.event_name == 'pull_request' + run: vendor/bin/pint --test --diff=origin/${{ github.base_ref }} + + - name: Run tests + run: vendor/bin/pest --compact + + audit: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.5' + coverage: none + + - name: Audit Composer packages + run: composer audit --locked diff --git a/README.md b/README.md index 0f66f31..56dd2a5 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,13 @@ ## Deployment -- Deployments run through **GitHub Actions + Deployer 8** (`.github/workflows/deploy.yml`, `deploy.php`, `deploy/custom.php`). -- **Production:** pushing or merging to `main` calculates the next version, deploys to production (`3.142.169.134`), and creates a GitHub release. Add `[skip deploy]` or `[no deploy]` to the commit message to push without deploying. +- Deployments run through **GitHub Actions + Deployer 8** (`.github/workflows/deploy.yml`, `deploy.php`, `deploy/custom.php`). `deploy.yml` calls `deploy-environment.yml` for each environment. +- **Tests:** `.github/workflows/tests.yml` runs Pest on every pull request into `main` or `development`, along with Pint on the changed files and `composer audit`. Every deploy runs the tests first and stops if they fail. The tests use SQLite in memory and no built assets (`withoutMix()` in `tests/TestCase.php`). +- **Production:** pushing or merging to `main` runs the tests, calculates the next version, deploys to production (`3.142.169.134`), and creates a GitHub release with notes listing the pull requests since the previous release. Add `[skip deploy]` or `[no deploy]` to the commit message to push without deploying. - **Development:** pushes to `development` don't deploy. Run the workflow manually (Actions → Build and Deploy → Run workflow → `development`) to deploy to the development server (`3.138.217.206`). +- Only one deploy runs at a time per branch; a second one waits. +- The deploy trusts only the SSH host keys in `.github/known_hosts`. If a server's keys change, regenerate the file with `ssh-keyscan -t ed25519,ecdsa,rsa ` after checking the fingerprints. +- Dependabot (`.github/dependabot.yml`) opens weekly update pull requests into `development` for GitHub Actions, Composer and npm. - Assets are built in CI and downloaded as an artifact; the server doesn't build them. - Releases live under `/data/web/digitizationacademy/releases/`, with `current` pointing at the active one. Both servers serve it with PHP 8.5 (`php8.5-fpm`). - nginx passes PHP the resolved release path (`fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;` and `fastcgi_param DOCUMENT_ROOT $realpath_root;` after `include fastcgi_params;`), so a new release is served without an OPcache reset. Keep both lines in the site config on each server. diff --git a/phpunit.xml b/phpunit.xml index 6f95223..0a76377 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -18,8 +18,10 @@ - + + + diff --git a/tests/TestCase.php b/tests/TestCase.php index 2932d4a..2115755 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -7,4 +7,12 @@ abstract class TestCase extends BaseTestCase { use CreatesApplication; + + protected function setUp(): void + { + parent::setUp(); + + // CI runs the tests before assets are built, so there's no mix-manifest.json + $this->withoutMix(); + } }