diff --git a/.circleci/config.yml b/.circleci/config.yml index 18648e3..12834ee 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -27,4 +27,4 @@ jobs: command: npm install --legacy-peer-deps - run: name: release - command: npm run semantic-release || true + command: npm run semantic-release || true \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..9761b8b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,43 @@ +name: Release + +on: + push: + branches: + - trunk + - alpha + - 'hotfix/**' + - 'epic/**' + +jobs: + release: + name: Build and Release + runs-on: ubuntu-latest + permissions: + contents: write + issues: write + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'npm' + + - name: Verify Node version + run: node --version + + - name: Install dependencies + run: npm ci --legacy-peer-deps + + - name: Release + env: + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm run semantic-release diff --git a/.github/workflows/reusable-build-distributable.yml b/.github/workflows/reusable-build-distributable.yml new file mode 100644 index 0000000..8c8c263 --- /dev/null +++ b/.github/workflows/reusable-build-distributable.yml @@ -0,0 +1,50 @@ +name: Build Distributable + +on: + workflow_call: + inputs: + archive-name: + description: 'Name of the ZIP archive distributable' + required: true + type: string + +jobs: + build-distributable: + name: Build distributable files + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'npm' + + - name: Verify Node version + run: node --version + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + tools: composer + + - name: Install dependencies + run: npm ci --legacy-peer-deps + + - name: Install rsync + run: sudo apt-get update && sudo apt-get install -y rsync + + - name: Install PHP packages + run: composer install --no-dev --no-scripts + + - name: Build plugin files + run: npm run build && npm run release:archive + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.archive-name }} + path: release/${{ inputs.archive-name }}.zip diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml new file mode 100644 index 0000000..97267a2 --- /dev/null +++ b/.github/workflows/reusable-build.yml @@ -0,0 +1,24 @@ +name: Build + +on: + workflow_call: + +jobs: + build: + name: Install node dependencies + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'npm' + + - name: Verify Node version + run: node --version + + - name: Install dependencies + run: npm ci --legacy-peer-deps diff --git a/.github/workflows/reusable-check-typescript.yml b/.github/workflows/reusable-check-typescript.yml new file mode 100644 index 0000000..b754310 --- /dev/null +++ b/.github/workflows/reusable-check-typescript.yml @@ -0,0 +1,27 @@ +name: Check TypeScript + +on: + workflow_call: + +jobs: + check-typescript: + name: Validate TypeScript + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'npm' + + - name: Verify Node version + run: node --version + + - name: Install dependencies + run: npm ci --legacy-peer-deps + + - name: Validate TypeScript + run: npm run typescript:check diff --git a/.github/workflows/reusable-generate-docs.yml b/.github/workflows/reusable-generate-docs.yml new file mode 100644 index 0000000..232c08c --- /dev/null +++ b/.github/workflows/reusable-generate-docs.yml @@ -0,0 +1,39 @@ +name: Generate Docs + +permissions: + contents: write + +on: + workflow_call: + +jobs: + generate-docs: + name: Generate documentation + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y graphviz plantuml + + - name: Download PHPDocumentor + run: | + curl -L -o ./phpdocumentor https://phpdoc.org/phpDocumentor.phar + chmod +x ./phpdocumentor + + - name: Generate documentation + run: ./phpdocumentor run -d . -t ./docs + + - name: Switch to docs branch, commit, and push + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GIT_COMMITTER_NAME: ${{ vars.GIT_COMMITTER_NAME || 'github-actions[bot]' }} + GIT_COMMITTER_EMAIL: ${{ vars.GIT_COMMITTER_EMAIL || 'github-actions[bot]@users.noreply.github.com' }} + run: | + git config user.name "$GIT_COMMITTER_NAME" + git config user.email "$GIT_COMMITTER_EMAIL" + git checkout -b docs + git add -f docs + git commit -m "Update the docs" + git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git" docs --force diff --git a/.github/workflows/reusable-i18n.yml b/.github/workflows/reusable-i18n.yml new file mode 100644 index 0000000..89f5aaa --- /dev/null +++ b/.github/workflows/reusable-i18n.yml @@ -0,0 +1,136 @@ +name: i18n + +on: + workflow_call: + +permissions: + contents: write + +jobs: + i18n: + name: Create translation files + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'npm' + + - name: Check if commit author is bot + id: check-bot + env: + GIT_COMMITTER_EMAIL: ${{ vars.GIT_COMMITTER_EMAIL || 'github-actions[bot]@users.noreply.github.com' }} + run: | + COMMIT_AUTHOR=$(git log -1 --pretty=format:'%ae') + if [ "$COMMIT_AUTHOR" = "$GIT_COMMITTER_EMAIL" ]; then + echo "Commit was made by bot ($(git rev-parse --short HEAD)), skipping translation update" + echo "skip=true" >> $GITHUB_OUTPUT + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + + - name: Install dependencies + if: steps.check-bot.outputs.skip != 'true' + run: npm ci --legacy-peer-deps + + - name: Configure PHP for deep nesting + if: steps.check-bot.outputs.skip != 'true' + run: | + # Increase xdebug max_nesting_level to handle deeply nested JS files + echo "xdebug.max_nesting_level=512" | sudo tee -a $(php -i | grep "Scan this dir for additional .ini files" | cut -d' ' -f9)/99-custom.ini + + - name: Install WP CLI + if: steps.check-bot.outputs.skip != 'true' + run: | + curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar + chmod +x wp-cli.phar + sudo mv wp-cli.phar /usr/local/bin/wp + + - name: Build plugin files + if: steps.check-bot.outputs.skip != 'true' + run: npm run build || true + + - name: Create POT translation files + if: steps.check-bot.outputs.skip != 'true' + env: + REPO_NAME: ${{ github.event.repository.name }} + run: | + # Theme is excluded, more in https://github.com/Automattic/newspack-theme/pull/2458 + if [ "$REPO_NAME" = "newspack-theme" ]; then + cd ./newspack-theme + wp i18n make-pot . languages/${REPO_NAME}.pot --exclude='src' --domain=${REPO_NAME} + cd - + else + wp i18n make-pot . languages/${REPO_NAME}.pot --domain=${REPO_NAME} + fi + + - name: Create JSON translation files + if: steps.check-bot.outputs.skip != 'true' + env: + REPO_NAME: ${{ github.event.repository.name }} + run: | + if [ "$REPO_NAME" = "newspack-theme" ]; then + cd ./newspack-theme + fi + + sudo apt-get update && sudo apt-get install -y gettext + + cd languages + + # Create PO files from POT if they don't exist + if [ ! -f "${REPO_NAME}-en_US.po" ]; then + echo "Creating ${REPO_NAME}-en_US.po from POT file" + wp i18n update-po ${REPO_NAME}.pot . + else + echo "${REPO_NAME}-en_US.po file already exists, skipping creation" + fi + + for po in *.po; do + if [ -f "$po" ]; then + echo "Processing file $po …" + # Update translations according to the new POT file + msgmerge $po $REPO_NAME.pot -o $po.out + mv $po.out $po + msgfmt $po -o $(basename $po .po).mo + # no-purge since we need the JS translations for the next run + wp i18n make-json --no-purge $po . + fi + done + + - name: Commit translation files + if: steps.check-bot.outputs.skip != 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO_NAME: ${{ github.event.repository.name }} + GIT_COMMITTER_NAME: ${{ vars.GIT_COMMITTER_NAME || 'github-actions[bot]' }} + GIT_COMMITTER_EMAIL: ${{ vars.GIT_COMMITTER_EMAIL || 'github-actions[bot]@users.noreply.github.com' }} + run: | + if [ "$REPO_NAME" = "newspack-theme" ]; then + cd ./newspack-theme + fi + if [ -d "languages" ]; then + LINES_CHANGED=$(git diff --numstat languages/ | awk '{sum += $1 + $2} END {print sum}') + # If no existing files were changed, check for new files + if [ -z "$LINES_CHANGED" ] || [ "$LINES_CHANGED" -eq 0 ]; then + LINES_CHANGED=$(git ls-files --others --exclude-standard languages/ | xargs wc -l 2>/dev/null | tail -1 | awk '{print $1}') + fi + else + LINES_CHANGED=0 + fi + LINES_CHANGED=${LINES_CHANGED:-0} + echo "Lines changed in languages/: $LINES_CHANGED" + if [ "$LINES_CHANGED" -gt 3 ]; then + git config user.email "$GIT_COMMITTER_EMAIL" + git config user.name "$GIT_COMMITTER_NAME" + git remote set-url origin https://x-access-token:$GITHUB_TOKEN@github.com/${{ github.repository }}.git + git add languages/ + git commit -m "chore: update translation files [skip ci]" + git pull --rebase origin ${{ github.ref_name }} + git push origin ${{ github.ref_name }} + fi diff --git a/.github/workflows/reusable-lint-js-scss.yml b/.github/workflows/reusable-lint-js-scss.yml new file mode 100644 index 0000000..95f016d --- /dev/null +++ b/.github/workflows/reusable-lint-js-scss.yml @@ -0,0 +1,28 @@ +name: Lint JS & SCSS + +on: + workflow_call: + +jobs: + lint-js-scss: + name: Lint JS & SCSS files + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'npm' + + - name: Verify Node version + run: node --version + + - name: Install dependencies + run: npm ci --legacy-peer-deps + + - name: Run Linter + # Temporarily skip linting SCSS due to stylelint config updates. Remove :js when ready to re-enable linting of SCSS. + run: npm run lint:js diff --git a/.github/workflows/reusable-lint-php.yml b/.github/workflows/reusable-lint-php.yml new file mode 100644 index 0000000..60135c2 --- /dev/null +++ b/.github/workflows/reusable-lint-php.yml @@ -0,0 +1,30 @@ +name: Lint PHP + +on: + workflow_call: + inputs: + php-version: + description: 'PHP version to use' + required: false + type: string + default: '8.3' + +jobs: + lint-php: + name: Lint PHP files + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ inputs.php-version }} + tools: composer + + - name: Install Composer dependencies + run: composer install + + - name: Lint PHP files + run: ./vendor/bin/phpcs diff --git a/.github/workflows/reusable-post-release.yml b/.github/workflows/reusable-post-release.yml new file mode 100644 index 0000000..f118f98 --- /dev/null +++ b/.github/workflows/reusable-post-release.yml @@ -0,0 +1,41 @@ +name: Post Release + +permissions: + contents: write + +on: + workflow_call: + secrets: + SLACK_CHANNEL_ID: + required: false + SLACK_AUTH_TOKEN: + required: false + +jobs: + post-release: + name: Perform post-release tasks + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'npm' + + - name: Install dependencies + run: npm ci --legacy-peer-deps + + - name: Perform post-release chores + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + GIT_COMMITTER_NAME: github-actions[bot] + GITHUB_COMMITER_EMAIL: github-actions[bot]@users.noreply.github.com + SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }} + SLACK_AUTH_TOKEN: ${{ secrets.SLACK_AUTH_TOKEN }} + run: ./node_modules/newspack-scripts/scripts/github/post-release.sh diff --git a/.github/workflows/reusable-release-wporg.yml b/.github/workflows/reusable-release-wporg.yml new file mode 100644 index 0000000..79c7535 --- /dev/null +++ b/.github/workflows/reusable-release-wporg.yml @@ -0,0 +1,58 @@ +# Release to WordPress.org. +# Requires the calling workflow to run reusable-release.yml first (which uploads release artifacts). +name: Release to WordPress.org + +on: + workflow_call: + inputs: + plugin-name: + description: 'WordPress.org plugin slug. Defaults to the repository name.' + required: false + type: string + default: '' + secrets: + WP_ORG_USERNAME: + required: true + WP_ORG_PASSWORD: + required: true + +jobs: + release-wporg: + name: Release to WordPress.org + runs-on: ubuntu-latest + env: + PLUGIN_NAME: ${{ inputs.plugin-name || github.event.repository.name }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'npm' + + - name: Install dependencies + run: npm ci --legacy-peer-deps + + - name: Download release artifacts + uses: actions/download-artifact@v4 + with: + name: release-artifacts + path: release/ + + - name: Verify release artifacts + run: | + if [ ! -d "release/$PLUGIN_NAME" ]; then + echo "::error::release/$PLUGIN_NAME not found. The release job must produce this directory via release:archive." + exit 1 + fi + + - name: Release to WordPress.org + env: + WP_ORG_USERNAME: ${{ secrets.WP_ORG_USERNAME }} + WP_ORG_PASSWORD: ${{ secrets.WP_ORG_PASSWORD }} + WP_ORG_PLUGIN_NAME: ${{ env.PLUGIN_NAME }} + run: ./node_modules/newspack-scripts/scripts/github/release-wporg.sh diff --git a/.github/workflows/reusable-release.yml b/.github/workflows/reusable-release.yml new file mode 100644 index 0000000..cc8589e --- /dev/null +++ b/.github/workflows/reusable-release.yml @@ -0,0 +1,61 @@ +name: Release + +on: + workflow_call: + secrets: + NPM_TOKEN: + required: false + +jobs: + release: + name: Release new version + runs-on: ubuntu-latest + permissions: + contents: write + issues: write + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'npm' + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + tools: composer + + - name: Verify Node version + run: node --version + + - name: Install dependencies + run: npm ci --legacy-peer-deps + + - name: Install rsync + run: sudo apt-get update && sudo apt-get install -y rsync + + - name: Install PHP packages + run: composer install --no-dev --no-scripts + + - name: Release new version + env: + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm run release + + - name: Upload release artifacts + uses: actions/upload-artifact@v4 + with: + name: release-artifacts + path: release/ + if-no-files-found: ignore + retention-days: 1 diff --git a/.github/workflows/reusable-test-js.yml b/.github/workflows/reusable-test-js.yml new file mode 100644 index 0000000..e5dd261 --- /dev/null +++ b/.github/workflows/reusable-test-js.yml @@ -0,0 +1,27 @@ +name: Test JS + +on: + workflow_call: + +jobs: + test-js: + name: Run JS tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'npm' + + - name: Verify Node version + run: node --version + + - name: Install dependencies + run: npm ci --legacy-peer-deps + + - name: Run JS Tests + run: npm run test diff --git a/.github/workflows/reusable-test-php.yml b/.github/workflows/reusable-test-php.yml new file mode 100644 index 0000000..2d7184a --- /dev/null +++ b/.github/workflows/reusable-test-php.yml @@ -0,0 +1,72 @@ +name: Test PHP + +on: + workflow_call: + inputs: + php-version: + description: 'PHP version to use' + required: false + type: string + default: '8.3' + wp-version: + description: 'WordPress version to test against' + required: false + type: string + default: 'latest' + secrets: + CODECOV_TOKEN: + required: false + +jobs: + test-php: + name: Run PHP tests + runs-on: ubuntu-latest + services: + mysql: + image: mysql:5.7 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: yes + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping" + --health-interval=10s + --health-timeout=5s + --health-retries=3 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + WP_TESTS_DIR: /tmp/wordpress-tests-lib + WP_CORE_DIR: /tmp/wordpress/ + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ inputs.php-version }} + tools: composer + extensions: mysqli + coverage: pcov + + - name: Install system dependencies + run: sudo apt-get update && sudo apt-get install -y subversion default-mysql-client + + - name: Install Composer dependencies + run: composer update + + - name: Setup WordPress test environment + run: | + rm -rf $WP_TESTS_DIR $WP_CORE_DIR + bash bin/install-wp-tests.sh wordpress_test root '' 127.0.0.1 ${{ inputs.wp-version }} + + - name: Run tests with coverage + run: vendor/bin/phpunit --coverage-clover coverage.xml + + - name: Upload coverage to Codecov + if: ${{ env.CODECOV_TOKEN != '' }} + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: coverage.xml + fail_ci_if_error: false diff --git a/bin/newspack-scripts.js b/bin/newspack-scripts.js index 550e3a4..01e41a5 100755 --- a/bin/newspack-scripts.js +++ b/bin/newspack-scripts.js @@ -1,10 +1,33 @@ #!/usr/bin/env node +const fs = require( 'fs' ); +const path = require( 'path' ); const spawn = require( 'cross-spawn' ); const utils = require( '../scripts/utils/index.js' ); const [ scriptName, ...nodeArgs ] = process.argv.slice( 2 ); +/** + * Resolve script path. If running in GitHub Actions, try to find the script + * in `scripts/github/` first, otherwise fall back to `scripts/`. + * + * @param {string} name Script name. + * @return {string} Resolved script path. + */ +const resolveScript = ( name ) => { + if ( process.env.GITHUB_ACTIONS ) { + const githubScriptPath = path.resolve( + __dirname, + '../scripts/github/', + name + '.js' + ); + if ( fs.existsSync( githubScriptPath ) ) { + return githubScriptPath; + } + } + return require.resolve( '../scripts/' + name ); +}; + if ( [ 'test', @@ -18,7 +41,7 @@ if ( ) { const result = spawn.sync( process.execPath, - [ require.resolve( '../scripts/' + scriptName ), ...nodeArgs ], + [ resolveScript( scriptName ), ...nodeArgs ], { stdio: 'inherit' } ); if ( result.signal ) { diff --git a/scripts/github/post-release.sh b/scripts/github/post-release.sh new file mode 100755 index 0000000..b8a1c19 --- /dev/null +++ b/scripts/github/post-release.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash + +# This script should be ran on CI after a new regular (not pre-release) version is released. + +git config user.name "$GIT_COMMITTER_NAME" +git config user.email "$GITHUB_COMMITER_EMAIL" + +# The last commit message at this point is the automated release commit. The second-to-last +# commit message should contain data about the merge. +SECOND_TO_LAST_COMMIT_MSG=$(git log -n 1 --skip 1 --pretty=format:"%s") + +LATEST_VERSION_TAG=$(git describe --tags --abbrev=0) + +git pull origin release +git checkout alpha + +# If the merge was from alpha branch (the basic flow), alpha branch should be reset. +if [[ $(echo $SECOND_TO_LAST_COMMIT_MSG | grep '^Merge .*alpha') ]]; then + echo '[newspack-scripts] Release was created from the alpha branch. Alpha branch will now be reset.' + + # Reset the tip of alpha branch to the release branch. + # The alpha branch is single-serving, just for alpha releases. After a release, + # we don't care about any alpha changes. + git reset --hard release -- + # Force-push the alpha branch. + git push "https://$GITHUB_TOKEN@github.com/${GITHUB_REPOSITORY}.git" --force +else + echo '[newspack-scripts] Release was created from a different branch than the alpha branch (e.g. a hotfix branch).' + echo '[newspack-scripts] Alpha branch will now be updated with the lastest changes from release.' + git merge --no-ff release -m "chore(release): merge in release $LATEST_VERSION_TAG" + if [[ $? == 0 ]]; then + git push "https://$GITHUB_TOKEN@github.com/${GITHUB_REPOSITORY}.git" + else + git merge --abort + echo '[newspack-scripts] Post-release merge to alpha failed.' + if [ -z "$SLACK_CHANNEL_ID" ] || [ -z "$SLACK_AUTH_TOKEN" ]; then + echo '[newspack-scripts] Missing Slack channel ID and/or token. Cannot notify.' + else + echo '[newspack-scripts] Notifying the team on Slack.' + curl \ + --data "{\"channel\":\"$SLACK_CHANNEL_ID\",\"blocks\":[{\"type\":\"section\",\"text\":{\"type\":\"mrkdwn\",\"text\":\"⚠️ Post-release merge to alpha failed for: \`$CIRCLE_PROJECT_REPONAME\`. Check <$CIRCLE_BUILD_URL|the build> for details.\"}}]}" \ + -H "Content-type: application/json" \ + -H "Authorization: Bearer $SLACK_AUTH_TOKEN" \ + -X POST https://slack.com/api/chat.postMessage \ + -s > /dev/null + fi + fi +fi + +# Update trunk branch with latest changes from the release branch, so they are in sync. +echo '[newspack-scripts] Merging the release branch into trunk.' +git checkout trunk + +# Merge release branch into trunk branch, and notify the team if any conflicts arise. +git merge --no-ff release -m "chore(release): merge in release $LATEST_VERSION_TAG" +if [[ $? == 0 ]]; then + echo '[newspack-scripts] Pushing updated trunk to origin.' + git push "https://$GITHUB_TOKEN@github.com/${GITHUB_REPOSITORY}.git" +else + git merge --abort + echo '[newspack-scripts] Post-release merge to trunk failed.' + if [ -z "$SLACK_CHANNEL_ID" ] || [ -z "$SLACK_AUTH_TOKEN" ]; then + echo '[newspack-scripts] Missing Slack channel ID and/or token. Cannot notify.' + else + echo '[newspack-scripts] Notifying the team on Slack.' + curl \ + --data "{\"channel\":\"$SLACK_CHANNEL_ID\",\"blocks\":[{\"type\":\"section\",\"text\":{\"type\":\"mrkdwn\",\"text\":\"⚠️ Post-release merge to \`trunk\` failed for: \`$CIRCLE_PROJECT_REPONAME\`. Check <$CIRCLE_BUILD_URL|the build> for details.\"}}]}" \ + -H "Content-type: application/json" \ + -H "Authorization: Bearer $SLACK_AUTH_TOKEN" \ + -X POST https://slack.com/api/chat.postMessage \ + -s > /dev/null + fi +fi diff --git a/scripts/github/release-wporg.sh b/scripts/github/release-wporg.sh new file mode 100755 index 0000000..58acf66 --- /dev/null +++ b/scripts/github/release-wporg.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +# Release the latest version on wordpress.org plugin repository. +# To be run as part of CI workflow. +# Assumptions: +# - repository name matches the wordpress.org plugin name or WP_ORG_PLUGIN_NAME is set, +# - there is a `release` directory with the folder containing the files and named as the plugin in it +# Partially adapted from https://carlalexander.ca/continuous-deployment-wordpress-directory-circleci/ + +SVN_PLUGINS_URL="https://plugins.svn.wordpress.org" +SVN_REPO_LOCAL_PATH="release/svn" +SVN_REPO_URL="$SVN_PLUGINS_URL/$WP_ORG_PLUGIN_NAME" + +LATEST_GIT_TAG=$(git describe --tags `git rev-list --tags --max-count=1`) +# Remove the "v" at the beginning of the git tag +LATEST_SVN_TAG=${LATEST_GIT_TAG:1} + +mkdir -p $SVN_REPO_LOCAL_PATH && cd $SVN_REPO_LOCAL_PATH +sudo apt-get update +sudo apt-get install subversion + +# Check if the latest SVN tag exists already +TAG=$(svn ls "$SVN_REPO_URL/tags/$LATEST_SVN_TAG") +error=$? +if [ $error == 0 ]; then + # Tag exists, don't deploy + echo "Latest tag ($LATEST_SVN_TAG) already exists on the WordPress directory. No deployment needed!" + exit 0 +fi + +# Wait a moment to avoid a 429 by WPORG's server. +sleep 3 + +svn checkout -q "$SVN_REPO_URL" . + +rm -rf trunk + +cp -r "../$WP_ORG_PLUGIN_NAME" ./trunk +cp -r ./trunk "./tags/$LATEST_SVN_TAG" + +# Add new files to SVN +svn stat | grep '^?' | awk '{print $2}' | xargs -I x svn add x@ + +# Remove deleted files from SVN +svn stat | grep '^!' | awk '{print $2}' | xargs -I x svn rm --force x@ + +# Commit to SVN +svn ci --no-auth-cache --username $WP_ORG_USERNAME --password $WP_ORG_PASSWORD -m "Deploy version $LATEST_SVN_TAG" diff --git a/scripts/github/release.js b/scripts/github/release.js new file mode 100644 index 0000000..f2b14c2 --- /dev/null +++ b/scripts/github/release.js @@ -0,0 +1,165 @@ +'use strict'; + +const utils = require( './utils/index.js' ); + +const semanticRelease = require( 'semantic-release' ); + +const { files, ...otherArgs } = require( 'yargs/yargs' )( + process.argv.slice( 2 ) +).parse(); + +const filesList = files.split( ',' ); + +// Get repository name from GitHub Actions environment variable (format: owner/repo). +const repoName = process.env.GITHUB_REPOSITORY?.split( '/' )[ 1 ] || 'unknown'; + +utils.log( `Releasing ${ repoName }…` ); + +const getConfig = ({ gitBranchName }) => { + const branchType = gitBranchName.split("/")[0]; + const githubConfig = { + assets: [ + { + path: `./release/${ repoName }.zip`, + label: `${ repoName }.zip`, + }, + ], + releasedLabels: false, + }; + + // Only post GH PR comments for alpha, hotfix/*, and release branches. + if ( ! ["alpha", "hotfix", "release"].includes(branchType) ) { + githubConfig.successComment = false; + githubConfig.failComment = false; + } + + // Only publish alpha and release branches to NPM. + const shouldPublishOnNPM = Boolean( process.env.NPM_TOKEN ) && ["alpha", "release"].includes(branchType); + if ( shouldPublishOnNPM ) { + utils.log( `Will publish to npm.` ); + } + + const config = { + dryRun: otherArgs.dryRun, + ci: otherArgs.ci, + debug: otherArgs.debug, + + branches: [ + // `release` branch is published on the main distribution channel (a new version on GH). + "release", + // `alpha` branch – for regular pre-releases. + { + name: "alpha", + prerelease: true, + }, + // `hotfix/*` branches – for releases outside of the release schedule. + { + name: "hotfix/*", + // With `prerelease: true`, the `name` would be used for the pre-release tag. A name with a `/` + // is not valid, though. See https://semver.org/#spec-item-9. + prerelease: '${name.replace(/\\//g, "-")}', + }, + // `epic/*` branches – for beta testing/QA pre-release builds. + { + name: "epic/*", + // With `prerelease: true`, the `name` would be used for the pre-release tag. A name with a `/` + // is not valid, though. See https://semver.org/#spec-item-9. + prerelease: '${name.replace(/\\//g, "-")}', + }, + ], + prepare: ["@semantic-release/changelog", "@semantic-release/npm"], + plugins: [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + [ + // Whether to publish on npm. + "@semantic-release/npm", + { + npmPublish: shouldPublishOnNPM, + }, + ], + "semantic-release-version-bump", + // Add the built ZIP archive to GH release. + [ + "@semantic-release/github", + githubConfig, + ], + ], + }; + + // Bump the semver and prepare a build package. + config.prepare.push( [ + // Increment the version in additional files, and the create the release archive. + 'semantic-release-version-bump', + { + files: filesList, + callback: 'npm run release:archive', + }, + ] ); + + // Unless on a hotfix or epic branch, add a commit that updates the files. + if ( [ 'hotfix', 'epic' ].indexOf( branchType ) === -1 ) { + let assets = filesList; + // These assets should be added to source control after a release. + if ( branchType === 'release' ) { + assets = [ + ...filesList, + 'package.json', + 'package-lock.json', + 'CHANGELOG.md', + ]; + } + utils.log( + `On ${ branchType } branch, following files will be updated: ${ assets.join( + ', ' + ) }` + ); + config.prepare.push( { + path: '@semantic-release/git', + assets, + message: + 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}', + } ); + } else { + utils.log( + `This branch is ${ branchType }, plugin files and the changelog will *not* be updated.` + ); + } + + return config; +}; + +const run = async () => { + try { + const gitBranch = await utils.getGitBranch(); + + const result = await semanticRelease.default( + getConfig( { gitBranchName: gitBranch } ) + ); + + if ( result ) { + const { lastRelease, commits, nextRelease, releases } = result; + + utils.log( + `Published ${ nextRelease.type } release version ${ nextRelease.version } containing ${ commits.length } commits.` + ); + + if ( lastRelease.version ) { + utils.log( `The last release was "${ lastRelease.version }".` ); + } + + for ( const release of releases ) { + utils.log( + `The release was published with plugin "${ release.pluginName }".` + ); + } + } else { + utils.log( 'No release published.' ); + } + } catch ( err ) { + console.error( 'The automated release failed with %O', err ); + process.exit( 1 ); + } +}; + +run();