laravel-octane-plugin 2.0.2 #1
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Validate | |
| on: | |
| pull_request: | |
| branches: [main] | |
| paths: | |
| - "plugins/**" | |
| - "scripts/**" | |
| - "schema/**" | |
| - ".github/workflows/validate.yml" | |
| # One-plugin-per-PR + semver-bump gate. Runs as pull_request_target so the | |
| # token is writable even for fork PRs (a plain pull_request token is read-only | |
| # on forks and would 403 the PR-edit API). pull_request_target runs in the | |
| # BASE repo's context, so the PR content is treated as untrusted DATA only: | |
| # we read composer.json with jq and never execute fork-supplied code with this | |
| # token. | |
| pull_request_target: | |
| branches: [main] | |
| paths: | |
| - "plugins/**" | |
| permissions: | |
| contents: read | |
| # Key by event too: the validate job (pull_request) and the meta gate | |
| # (pull_request_target) run from different events on the same PR and must not | |
| # cancel each other. | |
| concurrency: | |
| group: validate-${{ github.event_name }}-${{ github.event.pull_request.number }} | |
| cancel-in-progress: true | |
| jobs: | |
| validate: | |
| name: Validate plugins | |
| # The validate job reads the PR's code; it must only run on the read-only | |
| # `pull_request` event — never pull_request_target. | |
| if: github.event_name == 'pull_request' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| cache: "npm" | |
| - name: Setup PHP | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: "8.4" | |
| tools: composer | |
| - name: Install tooling | |
| run: npm ci | |
| - name: composer validate (changed plugins) | |
| run: | | |
| set -euo pipefail | |
| for dir in plugins/*/; do | |
| if [ -f "$dir/composer.json" ]; then | |
| echo "::group::composer validate $dir" | |
| composer validate --no-check-publish --no-check-all "$dir/composer.json" || true | |
| echo "::endgroup::" | |
| fi | |
| done | |
| - name: Validate all plugins | |
| run: node scripts/validate.mjs | |
| - name: Dry-run pack (prove deterministic zip + hash) | |
| run: node scripts/pack.mjs --dry-run | |
| pr-meta: | |
| name: Enforce version bump and PR title | |
| if: github.event_name == 'pull_request_target' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| # Untrusted checkout: PR head, including fork changes. Read as DATA only | |
| # (jq over composer.json). Never executed; credentials not persisted. | |
| - name: Checkout PR head (untrusted, data only) | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: ${{ github.event.pull_request.head.repo.full_name }} | |
| ref: ${{ github.event.pull_request.head.sha }} | |
| path: pr | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Determine changed plugin, verify composer.json, enforce bump | |
| id: meta | |
| working-directory: pr | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| set -euo pipefail | |
| git fetch --no-tags --depth=50 \ | |
| "https://github.com/${{ github.repository }}.git" "$BASE_SHA" 2>/dev/null || true | |
| if git cat-file -e "$BASE_SHA^{commit}" 2>/dev/null; then | |
| base="$BASE_SHA" | |
| else | |
| echo "::warning::base commit $BASE_SHA unavailable; falling back to origin/${{ github.event.pull_request.base.ref }}" | |
| base="origin/${{ github.event.pull_request.base.ref }}" | |
| fi | |
| if mb="$(git merge-base "$base" "$HEAD_SHA" 2>/dev/null)"; then | |
| diff_base="$mb" | |
| else | |
| echo "::warning::no merge base; comparing against base tip directly." | |
| diff_base="$base" | |
| fi | |
| changed="$(git diff --name-only "$diff_base" "$HEAD_SHA" -- plugins/ \ | |
| | awk -F/ 'NF>1 && $1=="plugins" {print $2}' \ | |
| | sort -u)" | |
| count="$(printf '%s' "$changed" | grep -c . || true)" | |
| if [ "$count" -eq 0 ]; then | |
| echo "No plugin changed (tooling-only PR); skipping title and bump checks." | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if [ "$count" -gt 1 ]; then | |
| echo "::error::This PR changes $count plugins:" | |
| printf '::error:: - %s\n' $changed | |
| echo "::error::Open one PR per plugin. The title must be a single '<name> <version>'." | |
| exit 1 | |
| fi | |
| slug="$changed" | |
| manifest="plugins/$slug/composer.json" | |
| if [ ! -f "$manifest" ]; then | |
| echo "::error::$manifest not found — cannot read the submission's version." | |
| exit 1 | |
| fi | |
| if ! pkg_name="$(jq -er '.name' "$manifest" 2>/dev/null)"; then | |
| echo "::error::$manifest is not valid JSON or has no top-level \"name\"." | |
| exit 1 | |
| fi | |
| if ! version="$(jq -er '.version' "$manifest" 2>/dev/null)"; then | |
| echo "::error::$manifest has no top-level \"version\". Add semver (e.g. \"1.0.0\")." | |
| exit 1 | |
| fi | |
| author_gh="$(jq -er '.extra.vito.author.github // empty' "$manifest" 2>/dev/null | sed 's/^@//' || true)" | |
| # Directory name must equal the package part of the composer name. | |
| pkg_part="${pkg_name##*/}" | |
| if [ "$pkg_part" != "$slug" ]; then | |
| echo "::error::composer name package part '$pkg_part' must equal directory name '$slug' (plugins/$slug/)." | |
| exit 1 | |
| fi | |
| semver_re='^[0-9]+\.[0-9]+\.[0-9]+([-+].+)?$' | |
| if ! printf '%s' "$version" | grep -Eq "$semver_re"; then | |
| echo "::error::version '$version' in $manifest is not valid semver (expected MAJOR.MINOR.PATCH)." | |
| exit 1 | |
| fi | |
| base_version="$(git show "$base:plugins/$slug/composer.json" 2>/dev/null \ | |
| | jq -er '.version' 2>/dev/null || true)" | |
| if [ -z "$base_version" ]; then | |
| echo "New plugin '$slug' at $version — no previous version to compare." | |
| else | |
| echo "Existing '$slug': $base_version (main) -> $version (PR)." | |
| core() { printf '%s' "$1" | sed -E 's/[-+].*$//'; } | |
| IFS=. read -r ba bi bp <<<"$(core "$base_version")" | |
| IFS=. read -r na ni np <<<"$(core "$version")" | |
| bumped=false | |
| if [ "$na" -gt "$ba" ]; then bumped=true | |
| elif [ "$na" -lt "$ba" ]; then bumped=down | |
| elif [ "$ni" -gt "$bi" ]; then bumped=true | |
| elif [ "$ni" -lt "$bi" ]; then bumped=down | |
| elif [ "$np" -gt "$bp" ]; then bumped=true | |
| elif [ "$np" -lt "$bp" ]; then bumped=down | |
| fi | |
| if [ "$bumped" = "down" ]; then | |
| echo "::error::version '$version' is lower than main's '$base_version'. Bump forward (no downgrades; published versions are immutable)." | |
| exit 1 | |
| fi | |
| if [ "$bumped" != "true" ]; then | |
| echo "::error::version '$version' does not bump main's '$base_version'. Increase MAJOR.MINOR.PATCH (published versions are immutable)." | |
| exit 1 | |
| fi | |
| fi | |
| title="$slug $version" | |
| echo "title=$title" >> "$GITHUB_OUTPUT" | |
| echo "slug=$slug" >> "$GITHUB_OUTPUT" | |
| echo "author_gh=$author_gh" >> "$GITHUB_OUTPUT" | |
| echo "skip=false" >> "$GITHUB_OUTPUT" | |
| echo "Resolved PR title: $title (author handle: ${author_gh:-<none>})" | |
| - name: Force PR title to "<name> <version>" | |
| if: steps.meta.outputs.skip == 'false' | |
| uses: actions/github-script@v7 | |
| env: | |
| DESIRED_TITLE: ${{ steps.meta.outputs.title }} | |
| with: | |
| script: | | |
| const desired = process.env.DESIRED_TITLE; | |
| const { owner, repo } = context.repo; | |
| const pull_number = context.payload.pull_request.number; | |
| const current = context.payload.pull_request.title; | |
| if (current === desired) { | |
| core.info(`PR title already "${desired}"; nothing to do.`); | |
| return; | |
| } | |
| await github.rest.pulls.update({ owner, repo, pull_number, title: desired }); | |
| core.info(`Forced PR title: "${current}" -> "${desired}".`); | |
| - name: Ping plugin author for review | |
| if: steps.meta.outputs.skip == 'false' && steps.meta.outputs.author_gh != '' | |
| uses: actions/github-script@v7 | |
| env: | |
| PLUGIN_SLUG: ${{ steps.meta.outputs.slug }} | |
| AUTHOR_GH: ${{ steps.meta.outputs.author_gh }} | |
| with: | |
| script: | | |
| const { owner, repo } = context.repo; | |
| const issue_number = context.payload.pull_request.number; | |
| const slug = process.env.PLUGIN_SLUG; | |
| const handle = process.env.AUTHOR_GH.replace(/^@/, "").trim(); | |
| const opener = (context.payload.pull_request.user?.login || "").toLowerCase(); | |
| const marker = "<!-- vito-plugin-author-review -->"; | |
| if (!handle || handle.toLowerCase() === opener) { | |
| core.info(`No author ping needed (handle '${handle}', opener '${opener}').`); | |
| return; | |
| } | |
| const comments = await github.paginate( | |
| github.rest.issues.listComments, | |
| { owner, repo, issue_number, per_page: 100 }, | |
| ); | |
| if (comments.some((c) => c.user?.type === "Bot" && c.body?.includes(marker))) { | |
| core.info("Author already pinged; nothing to do."); | |
| return; | |
| } | |
| await github.rest.issues.createComment({ | |
| owner, repo, issue_number, | |
| body: | |
| `${marker}\n@${handle} — this PR changes the **${slug}** plugin, ` + | |
| `which you're listed as the author of. Could you review it?`, | |
| }); | |
| core.info(`Pinged author @${handle} to review '${slug}'.`); |