Skip to content

Check plcc-ng Release #31

Check plcc-ng Release

Check plcc-ng Release #31

# 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"