From e180b4a9d4b835315c87caf5c72fb6860a790b5b Mon Sep 17 00:00:00 2001 From: Stoney Jackson Date: Mon, 27 Jul 2026 13:39:57 -0400 Subject: [PATCH 1/2] ci: mint short-lived App tokens instead of a static RELEASE_TOKEN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RELEASE_TOKEN had expired, and nothing surfaced it until the first push to main in months failed at checkout — taking the release with it. A PAT sitting in repo secrets has no expiry signal; it just stops working at whatever moment the next run happens to be. Both workflows now mint an installation token per run via the existing ourPLCC Release Bot, matching plcc-ng-devcontainer. The tokens last about an hour, so there is nothing long-lived left to rot. RELEASE_TOKEN is no longer referenced anywhere and can be deleted from the repo's secrets. Co-Authored-By: Claude Opus 5 --- .github/workflows/check-plcc-release.yml | 13 +++++++++++-- .github/workflows/release.yml | 15 +++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check-plcc-release.yml b/.github/workflows/check-plcc-release.yml index b0476a2..393f018 100644 --- a/.github/workflows/check-plcc-release.yml +++ b/.github/workflows/check-plcc-release.yml @@ -15,14 +15,23 @@ jobs: check: runs-on: ubuntu-latest steps: + # Minted per run and short-lived, so it cannot go stale between the + # weekly runs this workflow depends on. + - 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: ${{ secrets.RELEASE_TOKEN }} + token: ${{ steps.app-token.outputs.token }} - name: Check for new PLCC release env: - GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | # Fetch the latest PLCC release tag LATEST=$(gh api repos/ourPLCC/plcc/releases/latest --jq '.tag_name') diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8042d55..64af83a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,11 +16,22 @@ jobs: release: runs-on: ubuntu-latest steps: + # A static PAT rots silently: this one expired and only surfaced months + # later, failing checkout on the next push to main. App installation + # tokens are minted per run and expire in about an hour, so there is + # nothing long-lived to go stale. + - 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: fetch-depth: 0 - token: ${{ secrets.RELEASE_TOKEN }} + token: ${{ steps.app-token.outputs.token }} - name: Log in to GHCR uses: docker/login-action@v3 @@ -40,7 +51,7 @@ jobs: id: semantic uses: cycjimmy/semantic-release-action@v4 env: - GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} - name: Read PLCC version if: steps.semantic.outputs.new_release_published == 'true' From e0b7a6c83c19ce4cc8f10ea9b76ab132efe4b9a4 Mon Sep 17 00:00:00 2001 From: Stoney Jackson Date: Mon, 27 Jul 2026 13:55:24 -0400 Subject: [PATCH 2/2] ci: drop now-unneeded GITHUB_TOKEN scopes and refresh the setup docs Those permissions were granted for semantic-release and gh, both of which now authenticate with the App token. check-plcc-release needs no GITHUB_TOKEN scopes at all; release needs only packages:write, for the GHCR login. README still told maintainers to create a RELEASE_TOKEN and never mentioned APP_ID/APP_PRIVATE_KEY, so anyone setting this up from scratch would have configured the secret that just failed. Also corrects the commit convention: README claimed `feat!:` produces a major release. It does not. The Angular preset does not parse the `!` shorthand, so `feat!:` with no footer matches nothing and yields no release while the pipeline stays green. The BREAKING CHANGE footer is what triggers a major. Co-Authored-By: Claude Opus 5 --- .github/workflows/check-plcc-release.yml | 7 ++++--- .github/workflows/release.yml | 11 ++++++----- README.md | 21 +++++++++++++++++++-- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/.github/workflows/check-plcc-release.yml b/.github/workflows/check-plcc-release.yml index 393f018..84f6a00 100644 --- a/.github/workflows/check-plcc-release.yml +++ b/.github/workflows/check-plcc-release.yml @@ -7,13 +7,14 @@ on: - cron: '0 9 * * 1' # Every Monday at 09:00 UTC workflow_dispatch: # Allow manual trigger for testing -permissions: - contents: write - pull-requests: write +# Nothing here uses GITHUB_TOKEN any more: checkout and gh both authenticate +# with the App installation token, so GITHUB_TOKEN needs no scopes at all. +permissions: {} jobs: check: runs-on: ubuntu-latest + permissions: {} steps: # Minted per run and short-lived, so it cannot go stale between the # weekly runs this workflow depends on. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 64af83a..9e9a894 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,15 +6,16 @@ on: push: branches: [main] -permissions: - contents: write - packages: write - pull-requests: write - issues: write +# These grants applied to GITHUB_TOKEN, which semantic-release no longer +# uses — it authenticates with the App token now. The only remaining use of +# GITHUB_TOKEN is the GHCR login, so packages:write is all that is needed. +permissions: {} jobs: release: runs-on: ubuntu-latest + permissions: + packages: write steps: # A static PAT rots silently: this one expired and only surfaced months # later, failing checkout on the next push to main. App installation diff --git a/README.md b/README.md index 9586f00..c2aea00 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,17 @@ Commands available: `plccmk`, `scan`, `parse`, `rep` Use [Conventional Commits](https://www.conventionalcommits.org/) in your commit messages: - `fix:` — patch release - `feat:` — minor release -- `feat!:` or `BREAKING CHANGE:` — major release +- `feat:` **with a `BREAKING CHANGE:` footer** — major release +- `ci:`, `docs:`, `chore:`, `test:`, `refactor:` — no release + +Write the footer, not `feat!:`. semantic-release uses the Angular preset, +which does not parse the `!` shorthand: a `feat!:` subject with no footer +matches nothing and produces **no release at all**, which is easy to miss +because the pipeline still goes green. The `BREAKING CHANGE:` footer is what +actually triggers a major. + +If you squash-merge a major, make sure the footer survives into the squash +commit body — with it dropped, the release silently downgrades to a minor. ### Automated PLCC updates @@ -58,4 +68,11 @@ A weekly workflow checks for new PLCC releases and opens a PR automatically. If ### Release setup -The release workflow requires a `RELEASE_TOKEN` repository secret with a GitHub App token (preferred) or a classic PAT. Classic PAT scopes required: `repo`, `write:packages`. This is required because `GITHUB_TOKEN` cannot push to the protected `main` branch. +The release and PLCC-update workflows need two repository secrets, `APP_ID` and `APP_PRIVATE_KEY`, belonging to the **ourPLCC Release Bot** GitHub App. Each run mints its own short-lived installation token. This is required because `GITHUB_TOKEN` cannot push to the protected `main` branch. + +The App also needs, per repository: +- Contents and Pull requests set to **Read and write** +- Installation access granted to this repository — org-level installation does not cover new repos automatically +- A place in the `main` ruleset's bypass list, which only becomes selectable once repository access is granted + +An earlier setup used a static `RELEASE_TOKEN` PAT. That is no longer referenced by any workflow and can be deleted. It is worth knowing why it was replaced: a PAT gives no expiry signal, so when it lapsed nothing surfaced until the next push to main failed at checkout, months later.