diff --git a/.github/workflows/composer-version-guard.yml b/.github/workflows/composer-version-guard.yml new file mode 100644 index 0000000..75d8060 --- /dev/null +++ b/.github/workflows/composer-version-guard.yml @@ -0,0 +1,125 @@ +name: Guard composer.json version + +on: + pull_request: + branches: [develop] + +env: + GITHUB_TOKEN: ${{ secrets.GH_AUTOMATION_TOKEN }} + +jobs: + no-manual-version-bump: + name: Disallow manual composer.json version bumps + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write # zum Kommentieren/Löschen + issues: write # PR-Kommentare sind Issue-Kommentare + + env: + GH_TOKEN: ${{ github.token }} # gh CLI Auth + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + BASE_REF: ${{ github.base_ref }} + + steps: + - name: Checkout (current PR head) + uses: actions/checkout@v6 + + - name: Ensure jq and gh are available + run: | + jq --version + gh --version + + - name: Compare composer.json version via jq + id: check + run: | + set -euo pipefail + + # composer.json im Base-Branch RAW abrufen (stabiler als Git-Fetch) + base_json="$(gh api -H 'Accept: application/vnd.github.raw' \ + repos/${REPO}/contents/composer.json?ref=${BASE_REF} || true)" + + # Falls composer.json im Base nicht existiert (Mono-Repos, Erst-Add etc.) + if [ -z "${base_json}" ]; then + base_ver="" + else + base_ver="$(printf '%s' "${base_json}" | jq -r '.version // empty')" + fi + + # Head-Version aus dem Arbeitsverzeichnis + head_ver="$(jq -r '.version // empty' composer.json)" + + echo "Base version: ${base_ver}" + echo "Head version: ${head_ver}" + + if [ "${base_ver}" != "${head_ver}" ]; then + echo "has_version_change=true" >> "$GITHUB_OUTPUT" + else + echo "has_version_change=false" >> "$GITHUB_OUTPUT" + fi + + - name: Upsert PR comment if version changed + if: steps.check.outputs.has_version_change == 'true' + run: | + set -euo pipefail + MARK="" + + # Prüfen, ob schon ein Kommentar mit unserem Marker existiert + existing_ids=$(gh api --paginate \ + repos/${REPO}/issues/${PR_NUMBER}/comments \ + --jq "[ .[] | select( (.body // \"\") | contains(\"${MARK}\") ) | .id ] | .[]" || true) + + # Kommentar-Text vorbereiten (mit Marker, damit wir ihn später sicher finden/löschen) + cat > /tmp/comment.md <<'EOF' + + ⚠️ **Bitte die `version` in `composer.json` nicht manuell ändern.** + + Releases werden automatisch erstellt (.github/workflows/release.yml). + Dieser PR ändert die Eigenschaft `version` – bitte die Änderung zurücknehmen. + Sobald die Version wieder unverändert ist, wird dieser Hinweis automatisch entfernt. + EOF + + if [ -z "${existing_ids}" ]; then + # Neuen Kommentar erstellen + gh pr comment "${PR_NUMBER}" --body-file /tmp/comment.md + else + # Optional: vorhandene(n) Kommentar auf den aktuellen Text setzen (idempotent) + for id in ${existing_ids}; do + gh api \ + --method PATCH \ + "repos/${REPO}/issues/comments/${id}" \ + -f body="@/tmp/comment.md" + done + fi + + - name: Remove previous PR comments when resolved + if: steps.check.outputs.has_version_change != 'true' + run: | + set -euo pipefail + MARK="" + + # Alle Kommentare mit unserem Marker einsammeln + ids=$(gh api --paginate \ + repos/${REPO}/issues/${PR_NUMBER}/comments \ + --jq "[ .[] | select( (.body // \"\") | contains(\"${MARK}\") ) | .id ] | .[]" || true) + + if [ -n "${ids}" ]; then + echo "Removing ${ids}..." + for id in ${ids}; do + gh api -X DELETE "repos/${REPO}/issues/comments/${id}" || true + done + else + echo "No previous guard comments to remove." + fi + + - name: Fail the job if version changed + if: steps.check.outputs.has_version_change == 'true' + run: | + echo "::error::composer.json enthält eine Änderung an \"version\". Releases erfolgen automatisch – bitte diese Änderung entfernen." + exit 1 + + - name: Success + if: steps.check.outputs.has_version_change != 'true' + run: | + echo "OK: Keine manuelle Änderung an composer.json:version." diff --git a/.github/workflows/release-sync.yml b/.github/workflows/release-sync.yml new file mode 100644 index 0000000..2a5cacb --- /dev/null +++ b/.github/workflows/release-sync.yml @@ -0,0 +1,83 @@ +name: Sync release artifacts to develop + +on: + workflow_run: + workflows: ["Create release"] + types: [completed] + +permissions: + contents: write + +# Cannot use overall env because then the sync fails with the wrong user + +jobs: + sync-to-develop: + if: ${{ github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + + steps: + - name: Checkout (full history) + uses: actions/checkout@v6 + with: + fetch-depth: 0 + token: ${{ secrets.GH_AUTOMATION_TOKEN }} + + - name: Configure git & auth + run: | + git config user.name "${GIT_AUTHOR_NAME}" + git config user.email "${GIT_AUTHOR_EMAIL}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git" + + - name: Fetch branches (heads only, no tags; avoid ambiguous refs) + run: | + set -euo pipefail + + echo "Fetching main and develop branches..." + git fetch --no-tags --prune origin \ + +refs/heads/main:refs/remotes/origin/main \ + +refs/heads/develop:refs/remotes/origin/develop + + - name: Sync composer.json & CHANGELOG.md from main to develop + id: sync + run: | + set -euo pipefail + + echo "Configuring git user for pushing changes..." + git config user.email "websolutions@netlogix.de" + git config user.name "netlogix-bot" + + # Prüfe, ob sich genau diese Dateien zwischen main und develop unterscheiden + echo "Checking for differences in composer.json and CHANGELOG.md between main and develop..." + if git diff --quiet \ + refs/remotes/origin/develop..refs/remotes/origin/main \ + -- composer.json CHANGELOG.md; then + echo "nothing_to_sync=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Wechsle auf lokalen develop, der auf remote/develop basiert + echo "Checking out develop branch..." + git checkout -B develop refs/remotes/origin/develop + + # Übernehme die zwei Dateien exakt aus main + echo "Checking out composer.json and CHANGELOG.md from main branch..." + git checkout refs/remotes/origin/main -- composer.json CHANGELOG.md + + # Stage & Commit mit [skip ci], damit keine Workflows feuern + echo "Committing changes..." + git add composer.json CHANGELOG.md + git commit -m "[release-sync] chore: sync composer.json & CHANGELOG.md from main to develop [skip ci]" || { + echo "nothing_to_sync=true" >> "$GITHUB_OUTPUT" + exit 0 + } + + echo "Pushing changes to develop branch..." + git push origin HEAD:develop + echo "nothing_to_sync=false" >> "$GITHUB_OUTPUT" + echo "done" + env: + GITHUB_TOKEN: ${{ secrets.GH_AUTOMATION_TOKEN }} + + - name: Done + if: steps.sync.outputs.nothing_to_sync == 'true' + run: echo "Nichts zu syncen (composer.json/CHANGELOG.md identisch)." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..9081a1e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,29 @@ +name: Create release + +on: + push: + branches: + - main + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version: 20 + - name: Install Dependencies + run: npm install + - name: Semantic Release + uses: cycjimmy/semantic-release-action@v6 + id: semantic # Need an `id` for output variables + env: + GITHUB_TOKEN: ${{ secrets.GH_AUTOMATION_TOKEN }} diff --git a/.gitignore b/.gitignore index 7579f74..92d7233 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ +/.idea vendor composer.lock + +/node_modules/ \ No newline at end of file diff --git a/.releaserc b/.releaserc new file mode 100644 index 0000000..cd73ca3 --- /dev/null +++ b/.releaserc @@ -0,0 +1,31 @@ +{ + "branches": [ + "main", + ], + "plugins": [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + [ + "@semantic-release/changelog", + { + "changelogFile": "CHANGELOG.md" + } + ], + "@iwavesmedia/semantic-release-composer", + [ + "@semantic-release/git", + { + "assets": [ + "composer.json", + "CHANGELOG.md" + ] + } + ], + [ + "@semantic-release/github", + { + "successCommentCondition": "<% return issue.pull_request; %>" + } + ] + ] +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..53d94fe --- /dev/null +++ b/package-lock.json @@ -0,0 +1,29 @@ +{ + "name": "task-graph-solver", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@iwavesmedia/semantic-release-composer": "^1.0.0" + } + }, + "node_modules/@iwavesmedia/semantic-release-composer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@iwavesmedia/semantic-release-composer/-/semantic-release-composer-1.1.0.tgz", + "integrity": "sha512-qhvrNEHgvMEz6nF3C1jkaJkGlztU1wiQdI/BDYiDIq5N++ypy1tAPUT+a6iezqtNErWOoDU1Oi4lK3DklxfGGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@semantic-release/error": "^2.2.0" + } + }, + "node_modules/@semantic-release/error": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-2.2.0.tgz", + "integrity": "sha512-9Tj/qn+y2j+sjCI3Jd+qseGtHjOAeg7dU2/lVcqIQ9TV3QDaDXDYXcoOHU+7o2Hwh8L8ymL4gfuO7KxDs3q2zg==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c346452 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "@iwavesmedia/semantic-release-composer": "^1.0.0" + } +}