Repository navigation
Check plcc-ng Release #31
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
| # SPDX-FileCopyrightText: 2026 ourPLCC contributors | |
| # SPDX-License-Identifier: GPL-3.0-or-later | |
| name: Check plcc-ng Release | |
| on: | |
| schedule: | |
| - cron: '0 9 * * *' # Every day at 09:00 UTC | |
| workflow_dispatch: # Allow manual trigger for testing | |
| permissions: {} | |
| jobs: | |
| check: | |
| runs-on: ubuntu-latest | |
| permissions: {} | |
| steps: | |
| - name: Generate release bot token | |
| id: app-token | |
| uses: actions/create-github-app-token@v3 | |
| with: | |
| app-id: ${{ secrets.APP_ID }} | |
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| token: ${{ steps.app-token.outputs.token }} | |
| - name: Check for new plcc-ng release on PyPI | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| run: | | |
| LATEST=$(curl -fsSL https://pypi.org/pypi/plcc-ng/json | jq -r '.info.version') | |
| echo "Latest plcc-ng release: $LATEST" | |
| CURRENT=$(jq -r '.features["./features/plcc-ng"].version' \ | |
| images/plcc-ng/.devcontainer/devcontainer.json) | |
| echo "Currently pinned: $CURRENT" | |
| if [ "$LATEST" = "$CURRENT" ]; then | |
| echo "Already up to date. No action needed." | |
| exit 0 | |
| fi | |
| BRANCH="chore/update-plcc-ng-${LATEST}" | |
| # The image's major tag is the stability contract for courses | |
| # (docs/choosing-your-image.md tells them to pin it for a term), so a | |
| # new plcc-ng major has to release the image as a major too. Otherwise | |
| # a patch release would move :N onto a new major of the tool. | |
| # Deliberately `feat:` and not `feat!:` — the Angular preset that | |
| # semantic-release uses does not parse the `!` shorthand, so a | |
| # `feat!:` subject whose BREAKING CHANGE footer went missing yields | |
| # no release at all. `feat:` degrades to a minor instead. | |
| OLD_MAJOR="${CURRENT%%.*}" | |
| NEW_MAJOR="${LATEST%%.*}" | |
| REVIEW_NOTE="" | |
| if [ "$NEW_MAJOR" != "$OLD_MAJOR" ]; then | |
| MAJOR_BUMP=true | |
| COMMIT_SUBJECT="feat: update plcc-ng to ${LATEST}" | |
| COMMIT_BODY="BREAKING CHANGE: plcc-ng ${LATEST} is a new major version (was ${CURRENT}). Images already published under the previous major tag keep plcc-ng ${CURRENT}; this release publishes under a new major tag." | |
| # The image tag guidance is prose and cannot be rewritten safely by | |
| # a script: it names both the new major and the one being left | |
| # behind, so a blind :N -> :M substitution produces text that is | |
| # confidently wrong. Ask for a human instead. | |
| REVIEW_NOTE=$(printf '%s\n' \ | |
| "" \ | |
| "---" \ | |
| "" \ | |
| "### :warning: Major bump — needs a human eye" \ | |
| "" \ | |
| "\`README.md\`, \`devcontainer.json\` and \`docs/choosing-your-image.md\` **are** modified by this PR: every fully qualified image reference in them was retagged \`:${OLD_MAJOR}\` → \`:${NEW_MAJOR}\` automatically." \ | |
| "" \ | |
| "What was **not** rewritten is the surrounding guidance *text* in \`docs/choosing-your-image.md\`. It still describes the previous major, and a script cannot fix it safely — the wording names both the new major and the one being left behind. Please review these by hand:" \ | |
| "" \ | |
| "- the version tag table (\`${OLD_MAJOR}\`, \`${OLD_MAJOR}.1\`, \`${OLD_MAJOR}.1.3\`)" \ | |
| "- the \"pin the major tag\" recommendation" \ | |
| "- the paragraph explaining which plcc-ng line each tag carries — \`:${OLD_MAJOR}\` now becomes the frozen one, and \`:${NEW_MAJOR}\` the current one" \ | |
| "" \ | |
| "The feature reference \`ghcr.io/ourplcc/features/plcc-ng:1\` is deliberately untouched: it tracks the feature's own version, not plcc-ng's.") | |
| else | |
| MAJOR_BUMP=false | |
| COMMIT_SUBJECT="fix: update plcc-ng to ${LATEST}" | |
| COMMIT_BODY="" | |
| fi | |
| echo "Release type: $COMMIT_SUBJECT (major bump: $MAJOR_BUMP)" | |
| # Idempotency guard: keyed on the PR, not the branch. A branch with no | |
| # PR means an earlier run died between push and PR creation; that must | |
| # be recoverable, not mistaken for work already done. | |
| if [ -n "$(gh pr list --head "$BRANCH" --state all --json number --jq '.[].number')" ]; then | |
| echo "A PR for '$BRANCH' already exists. No action needed." | |
| exit 0 | |
| fi | |
| if git ls-remote --exit-code --heads origin "$BRANCH" > /dev/null 2>&1; then | |
| echo "Branch '$BRANCH' exists with no PR. Recovering by opening the PR." | |
| # actions/checkout configures a single-branch refspec, so no | |
| # origin/$BRANCH tracking ref exists here — use FETCH_HEAD. | |
| git fetch --depth=1 origin "$BRANCH" | |
| git checkout -B "$BRANCH" FETCH_HEAD | |
| else | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git checkout -b "$BRANCH" | |
| # Pinned version in both prebuilt images. | |
| for cfg in images/plcc-ng/.devcontainer/devcontainer.json \ | |
| images/plcc-ng-full/.devcontainer/devcontainer.json; do | |
| jq --arg v "$LATEST" \ | |
| '.features["./features/plcc-ng"].version = $v' \ | |
| "$cfg" > tmp.json | |
| mv tmp.json "$cfg" | |
| done | |
| # Feature test: the scenario pin and the assertion that checks it | |
| # must move together, or the test fails against its own scenario. | |
| jq --arg v "$LATEST" \ | |
| '.pinned_version.features["plcc-ng"].version = $v' \ | |
| test/plcc-ng/scenarios.json > tmp.json | |
| mv tmp.json test/plcc-ng/scenarios.json | |
| sed -i "s/^EXPECTED_VERSION=.*/EXPECTED_VERSION=\"${LATEST}\"/" \ | |
| test/plcc-ng/pinned_version.sh | |
| # Feature metadata names a concrete version in its proposals and | |
| # description. Bump the feature's own patch version so the edit is | |
| # actually published — publish skips already-published versions. | |
| # The description contains single quotes, so build it in the shell | |
| # and pass it as an argument rather than embedding it in the | |
| # single-quoted jq program. | |
| DESC="PyPI version of plcc-ng to install (e.g. '${LATEST}'), or 'latest' for the newest release." | |
| jq --arg v "$LATEST" --arg desc "$DESC" \ | |
| '.options.version.proposals = ["latest", $v] | |
| | .options.version.description = $desc | |
| | .version = ((.version | split(".")) as $p | |
| | "\($p[0]).\($p[1]).\($p[2] | tonumber + 1)")' \ | |
| src/plcc-ng/devcontainer-feature.json > tmp.json | |
| mv tmp.json src/plcc-ng/devcontainer-feature.json | |
| # Version example in the docs. Anchored on the backticks so it | |
| # cannot also rewrite the Java feature's { "version": "21" }. | |
| sed -i "s|\`{ \"version\": \"[0-9][^\"]*\" }\`|\`{ \"version\": \"${LATEST}\" }\`|" \ | |
| docs/choosing-your-image.md | |
| # On a major bump the published image tag changes, so the | |
| # copy-paste snippets naming it have to follow or they point at | |
| # the previous major line. | |
| # | |
| # perl, not sed: this needs a negative lookahead so that :2 does | |
| # not match inside :20 or :2.1, and BSD sed (what a maintainer | |
| # runs locally) lacks both \b and lookaheads. \Q..\E quotes the | |
| # dots and slashes in the interpolated image path. | |
| # | |
| # Matching the fully qualified path is what keeps | |
| # ghcr.io/ourplcc/features/plcc-ng:1 safe — that tag tracks the | |
| # feature's own version and must not move with plcc-ng's. | |
| if [ "$MAJOR_BUMP" = true ]; then | |
| echo "Retagging image references :${OLD_MAJOR} -> :${NEW_MAJOR}" | |
| for img in plcc-ng plcc-ng-full; do | |
| perl -pi -e \ | |
| "s{\Qghcr.io/ourplcc/devcontainers/${img}:${OLD_MAJOR}\E(?![0-9.])}{ghcr.io/ourplcc/devcontainers/${img}:${NEW_MAJOR}}g" \ | |
| README.md devcontainer.json docs/choosing-your-image.md | |
| done | |
| fi | |
| # README.md and devcontainer.json only change on a major bump; | |
| # git add is a no-op for them otherwise. | |
| git add images test src docs README.md devcontainer.json | |
| if [ -n "$COMMIT_BODY" ]; then | |
| git commit -m "$COMMIT_SUBJECT" -m "$COMMIT_BODY" | |
| else | |
| git commit -m "$COMMIT_SUBJECT" | |
| fi | |
| # Race-condition guard: treat push failure as no-op | |
| git push origin "$BRANCH" || { | |
| echo "Push failed — concurrent run. Exiting cleanly." | |
| exit 0 | |
| } | |
| fi | |
| # The PR title becomes the squash-merge commit subject, so it carries | |
| # the release type. The body carries the BREAKING CHANGE footer for | |
| # the same reason — on a major, both are what semantic-release reads. | |
| # | |
| # Do NOT move ${COMMIT_BODY} below ${REVIEW_NOTE} to "put the footer | |
| # last". Conventional Commits says footers come last, but the parser | |
| # disagrees in practice: with the markdown review note in between, | |
| # a squash of this body analyses as `minor` instead of `major` — | |
| # verified by running @semantic-release/commit-analyzer over both | |
| # orderings. The footer must precede the note. | |
| PR_URL=$(gh pr create \ | |
| --title "$COMMIT_SUBJECT" \ | |
| --body "$(cat <<EOF | |
| Automated update: plcc-ng has a new release (${LATEST}). | |
| CI will build and test both images on this PR. Merge if green — merging triggers the release workflow, which publishes new versioned images automatically. | |
| PyPI release: https://pypi.org/project/plcc-ng/${LATEST}/ | |
| ${COMMIT_BODY} | |
| ${REVIEW_NOTE} | |
| EOF | |
| )" \ | |
| --base main \ | |
| --head "$BRANCH") | |
| echo "Opened $PR_URL" | |
| # Cosmetic only — never fail the run over a missing label or scope. | |
| gh pr edit "$PR_URL" --add-label automated \ | |
| || echo "::warning::Could not apply the 'automated' label to $PR_URL" |