When running the Release job, we restore our `node_modules` cache via
`setup-node`. This leads to `semantic-release` executing - with
`contents: write`, `packages: write`, `id-token: write` and Docker Hub
credentials - the release process.
If we were to have a poisoned cache, this could lead to backdoored npm
packages and Docker images.
Although not currently exploitable, we should further harden this by:
- never using the cache for `release` jobs
- requiring a full `pnpm install --frozen-lockfile` on release jobs
- increasing the build time slightly on release jobs
This ensures that we don't leave ourselves vulnerable to cache poisoning
in the future.
It appears that zizmor's `cache-poisoning` audit covers this pattern,
but doesn't recognise a branch-push `semantic-release` job as a release
workflow, so won't flag regressions here.
Co-authored-by: Claude Fable 5 <jamie.tanna+claude-code@mend.io>
As per the previous commit, we currently don't lint our composite
actions, but should do.
Co-authored-by: Claude Fable 5 <jamie.tanna+claude-code@mend.io>
The Unified Agent scan passes `secrets.WS_ORG_TOKEN` as a `-apiKey`
CLI argument. Actions masks it in logs, but command-line arguments
are visible in the runner's process table to anything else running on
the machine.
As the Unified Agent reads any configuration parameter from a
`WS_`-prefixed environment variable, we can supply `WS_APIKEY` via
`env:` and drop the argument.
Co-authored-by: Claude Fable 5 <jamie.tanna+claude-code@mend.io>
To avoid potentially expanding a `discussion.created_at` event, we can
make sure we don't interpolate it, by injecting it in via `env`.
Co-authored-by: Claude Fable 5 <jamie.tanna+claude-code@mend.io>
To reduce risk of template injection, we can pass secrets and inputs as
an `env` input, which avoids shell expansion and hardens our usage.
Co-authored-by: Claude Fable 5 <jamie.tanna+claude-code@mend.io>
To make sure that we cover these steps explicitly, we can add a check
for them in `test-success`.
Co-authored-by: Claude Fable 5 <jamie.tanna+claude-code@mend.io>
As we're missing the `${{ }}`, we're passing in the literal string
`github.event_name ...` to `fail_ci_if_error` which may not evaluate as
expected.
Co-authored-by: Claude Fable 5 <jamie.tanna+claude-code@mend.io>
So we can run them both in parallel, speeding up the overall run time.
This may lead to a case where we run `build-docs` in the case a linting
failure is hit, but it also allows running them both in parallel which
could save ~1m of wasted waiting time.
Co-authored-by: Claude Fable 5 <jamie.tanna+claude-code@mend.io>
Like the vitest cache, the tsbuildinfo cache in `lint-other` can only
restore entries whose key contains the current
`hashFiles('pnpm-lock.yaml')` — its existing `restore-keys` prefix
still embeds the hash — so every lockfile change makes `type-check`
run a full, rather than incremental, `tsc`.
We can add a bare `tsbuildinfo-` fallback after the existing
same-hash prefix, restoring the newest cache from before the lockfile
bump. tsc records the version of every input file in the buildinfo
and recompiles whatever changed, so a stale restore is safe.
We intentionally leave the similarly-shaped prettier cache without a
cross-hash fallback: prettier can't detect plugin changes, so the
lockfile hash in its key is the only thing invalidating its cache
when a plugin updates — a cross-hash restore could wrongly skip
reformatting files.
Co-authored-by: Claude Fable 5 <jamie.tanna+claude-code@mend.io>
The vitest cache key in `test` embeds `hashFiles('pnpm-lock.yaml')`
and has no `restore-keys`, so every lockfile change starts each test
shard fully cold, re-transforming the whole codebase. As Renovate
updates its own lockfile near-daily, this is the common case rather
than the exception.
Because `restore-keys` performs prefix matching, it can only wildcard
the end of a key, so we first reorder the key to put
`matrix.cache-key` before the lockfile hash. The fallback then means
"same shard, any lockfile". vitest validates restored entries itself
(transforms are keyed by content hash), so a stale restore degrades
to a rebuild, never a wrong result.
Due to the reorder, the first run after this change won't match any
existing cache entry, so will start cold once.
Co-authored-by: Claude Fable 5 <jamie.tanna+claude-code@mend.io>
As `lint-actions` doesn't currently need any access to `pnpm` artifacts,
we can run this check earlier than needing to have `setup-build` (and a
cache hit or miss).
Co-authored-by: Claude Fable 5 <jamie.tanna+claude-code@mend.io>
As a way to get better visibility for - predominantly - transient issues
GitHub are having, via [0] we can auto-label PRs, so they can then be
picked up by a maintainer.
We can use the `-slim` runners, as this doesn't need much compute, and
it reduces ours and GitHub's costs.
[0]: https://www.jvt.me/posts/2026/08/11/github-merge-queue-prs/
Co-authored-by: Sonnet 5 <claude-code@jamietanna.co.uk>
* ci: migrate to `pnpm/setup`
As part of ongoing issues we're seeing related to
https://github.com/pnpm/action-setup/issues/277, we've fixed this
upstream. However, while investigating, the README notes that the
`pnpm/setup` Action should be used for pnpm 11+.
Co-authored-by: Claude Sonnet 5 <jamie.tanna+claude-code@mend.io>
* fixup! ci: migrate to `pnpm/setup`
---------
Co-authored-by: Claude Sonnet 5 <jamie.tanna+claude-code@mend.io>
As it doesn't work, as commits from `github-actions[bot]` can't be used
to trigger CI, and we don't want to use a PAT (as it'll then shows as
an author on PRs).
Renovate 44.0.0 was incorrectly released from #44004 due to the
`BREAKING ...` reference in the commit message.
Renovate uses Squash Merge as our method for merging commits into
`main`, and when enabled with the Merge Queue[0], this doesn't provide
maintainers with the ability to inspect the commit messages before
merging (which would be the case if not using the Merge Queue, we would
likely rewrite the commit message prior to merge.
With this in mind, Renovate maintainers often don't inspect the commit
messages, allowing them to stay as they are, as they'll be squashed into
the final commit.
In the case of #44004, this led to us missing the `BREAKING ...`
trailing message - from changes that were split into #44939 - and
because of this, semantic-release published this as a major release.
As a means to prevent this happening again, we can introduce a CI check
that performs a full `semantic-release --dry-run` programatically, so we
can avoid relying on log lines from `semantic-release`.
This then flags if there is a suggested major release, and if so,
rejecting it.
Co-authored-by: Claude Opus 4.8 (1M context) <jamie.tanna+claude-code@mend.io>