Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 69 additions & 26 deletions .github/workflows/check-plcc-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,40 +39,83 @@ jobs:

BRANCH="chore/update-plcc-${LATEST}"

# Check whether a PR branch already exists (idempotency guard)
if git ls-remote --exit-code --heads origin "$BRANCH" > /dev/null 2>&1; then
echo "Branch '$BRANCH' already exists. PR already open."
# The image's major tag is the stability contract for courses, so a
# new PLCC major has to release the image as a major too. Otherwise a
# patch release would move :N onto a new major of the tool. Tags are
# v-prefixed, so strip it before comparing.
# 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.
if [ "$(echo "${LATEST#v}" | cut -d. -f1)" != "$(echo "${CURRENT#v}" | cut -d. -f1)" ]; then
COMMIT_SUBJECT="feat: update PLCC to ${LATEST}"
COMMIT_BODY="BREAKING CHANGE: PLCC ${LATEST} is a new major version (was ${CURRENT}). Images already published under the previous major tag keep PLCC ${CURRENT}; this release publishes under a new major tag."
else
COMMIT_SUBJECT="fix: update PLCC to ${LATEST}"
COMMIT_BODY=""
fi
echo "Release type: $COMMIT_SUBJECT"

# 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

# Create update branch
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b "$BRANCH"
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"

# Update the pinned PLCC version in devcontainer.json
jq --arg v "$LATEST" \
'.features["./features/plcc"].version = $v' \
.devcontainer/devcontainer.json > tmp.json
mv tmp.json .devcontainer/devcontainer.json
# Update the pinned PLCC version in devcontainer.json
jq --arg v "$LATEST" \
'.features["./features/plcc"].version = $v' \
.devcontainer/devcontainer.json > tmp.json
mv tmp.json .devcontainer/devcontainer.json

git add .devcontainer/devcontainer.json
git commit -m "fix: update PLCC to ${LATEST}"
git add .devcontainer/devcontainer.json
if [ -n "$COMMIT_BODY" ]; then
git commit -m "$COMMIT_SUBJECT" -m "$COMMIT_BODY"
else
git commit -m "$COMMIT_SUBJECT"
fi

# Push branch; treat push failure as no-op (race condition guard)
git push origin "$BRANCH" || {
echo "Push failed — branch may already exist. Exiting cleanly."
exit 0
}
# Push branch; treat push failure as no-op (race condition guard)
git push origin "$BRANCH" || {
echo "Push failed — concurrent run. Exiting cleanly."
exit 0
}
fi

# Open PR
gh pr create \
--title "fix: update PLCC to ${LATEST}" \
--label "automated" \
# 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.
# Every line stays indented to the run block. A line at column 0 ends
# the YAML block scalar, which is what made this file unparseable and
# kept the workflow from ever running. YAML strips this indentation,
# so the body text itself is unaffected.
PR_URL=$(gh pr create \
--title "$COMMIT_SUBJECT" \
--body "Automated update: PLCC has a new release (${LATEST}).

CI will build and test the image on this PR. Merge if green — merging triggers the release workflow, which publishes a new versioned image automatically.
CI will build and test the image on this PR. Merge if green — merging triggers the release workflow, which publishes a new versioned image automatically.

PLCC release: https://github.com/ourPLCC/plcc/releases/tag/${LATEST}

PLCC release: https://github.com/ourPLCC/plcc/releases/tag/${LATEST}" \
${COMMIT_BODY}" \
Comment on lines +109 to +113
--base main \
--head "$BRANCH"
--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"
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,32 @@ jobs:
repo: context.repo.repo,
body: `🐳 **PR image built:** \`ghcr.io/ourplcc/plcc-devcontainer:pr-${{ github.event.pull_request.number }}\`\n\nTo test manually, update \`.devcontainer/devcontainer.json\` to use this image tag.`
})

# The single check the ruleset should require.
#
# The ruleset previously required "CI" — this workflow's top-level name.
# Actions posts one check per job, never one named after the workflow, so
# that check was never reported and every PR hung on "Expected — waiting
# for status to be reported" while all real checks were green. Requiring
# `build` would fix it today but break again the moment that job gains a
# matrix and its check becomes `build (x)`. This name never changes.
ci-gate:
needs: [build]
# Must run even when build fails — without this the gate is itself
# skipped on failure, reports nothing, and reproduces the same hang.
if: always()
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Require all CI jobs to have succeeded
env:
BUILD: ${{ needs.build.result }}
run: |
echo "build=$BUILD"
# Anything other than success — failure, cancelled, or skipped —
# must block the merge.
if [ "$BUILD" != "success" ]; then
echo "::error::CI did not fully succeed — build was '$BUILD'."
exit 1
fi
echo "All required CI jobs succeeded."
Loading