From 591e6d584703ace632c48ef37034829cb53b2b6a Mon Sep 17 00:00:00 2001 From: rbruhn Date: Sun, 11 Oct 2026 15:26:36 -0400 Subject: [PATCH 1/3] Run the tests in CI before deploying, like BioSpex Add tests.yml for pull requests and as the first job of every deploy, move the duplicated build-and-deploy job into deploy-environment.yml, trust the servers through a committed known_hosts file, run one deploy at a time per branch, generate release notes from merged pull requests, and add Dependabot. The tests call withoutMix() so they pass before assets are built. --- .github/dependabot.yml | 37 +++ .github/known_hosts | 8 + .github/workflows/deploy-environment.yml | 141 ++++++++++ .github/workflows/deploy.yml | 324 ++++------------------- .github/workflows/tests.yml | 83 ++++++ README.md | 8 +- tests/TestCase.php | 8 + 7 files changed, 338 insertions(+), 271 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/known_hosts create mode 100644 .github/workflows/deploy-environment.yml create mode 100644 .github/workflows/tests.yml 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/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(); + } } From 2d4c63a0680471eb15250b1dce3c1f1d9724ae26 Mon Sep 17 00:00:00 2001 From: rbruhn Date: Sun, 11 Oct 2026 15:30:44 -0400 Subject: [PATCH 2/3] Keep the tests off Redis The response cache defaults to the Redis store, so the tests failed on the CI runner, which has no Redis. Disable it and use the array store in phpunit.xml. --- phpunit.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/phpunit.xml b/phpunit.xml index 6f95223..174a5d0 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -20,6 +20,8 @@ + + From bcebf5bb68a39dd1bbafd2c2ddbad8dc682b742e Mon Sep 17 00:00:00 2001 From: rbruhn Date: Sun, 11 Oct 2026 15:33:38 -0400 Subject: [PATCH 3/3] Use the array mailer in tests Registering sends a verification email. With MAIL_MAILER=smtp the test needed a mail server on 127.0.0.1:1025, which the CI runner doesn't have. --- phpunit.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml b/phpunit.xml index 174a5d0..0a76377 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -18,7 +18,7 @@ - +