Skip to content

Least-privilege cleanup of the Entur workflows - #57

Open
vpaturet wants to merge 5 commits into
main_configfrom
ci/harden-release-workflow
Open

vpaturet wants to merge 5 commits into
main_configfrom
ci/harden-release-workflow

Conversation

@vpaturet

@vpaturet vpaturet commented Sep 15, 2026

Copy link
Copy Markdown

Routine least-privilege cleanup of the Entur-only workflows.

1. Credential handling in the release job (9b0b6e5)

The release script only fetches, and both repositories are public, so the checkout no longer takes a
token and no longer persists credentials. The push it makes at the end is redirected to a scratch
repository on the runner; the actual push to entur/main and the GitHub release happen in a final
step. script/custom-release.py is unchanged.

2. Allow-list for the "Entur Test" label (a5c1d2e)

A new job checks that the label on each included pull request was set by someone in
ENTUR_TEST_LABELERS, and writes the included pull requests, their authors and heads to the job
summary so the Approve step shows what a release contains. A hotfix skips the check, since it merges
nothing; there is no flag to skip it otherwise, as removing the label or adding the person to the
list is quicker than a rerun and leaves the next release correct too.

3. Pins and permissions (e957f13)

Actions pinned to the commit behind the tag already in use (not a version bump), explicit per-job
permissions, and tighter Maven cache restore-keys.

Per-job rather than workflow-wide permissions on purpose: a called workflow can only lower the
permissions it is given, so a workflow-level contents: read would remove the id-token: write
that gha-docker and gha-helm need for GCP auth.

@vpaturet vpaturet changed the title ci(release): keep the push credential out of the build phase and gate the Entur Test label ci: least-privilege cleanup of the Entur workflows Sep 15, 2026
@vpaturet
vpaturet force-pushed the ci/harden-release-workflow branch from e957f13 to c574b27 Compare September 15, 2026 12:04
The script only fetches from GitHub and both repositories are public, so the
checkout no longer takes a token and no longer persists credentials. The push
the script makes at the end is redirected to a scratch repository on the runner
by setting the push URL of the entur remote; fetching is unaffected.

The push to entur/main and the GitHub release move to a final step, which runs
once Maven is done. The GraphQL query for labeled pull requests uses the job
token, which is enough to read a public repository.

script/custom-release.py is unchanged. Also tighten the Maven cache
restore-keys and declare read-only permissions.
Add a job that verifies the "Entur Test" label on each included pull request was
set by someone in ENTUR_TEST_LABELERS, and writes the included pull requests,
their authors, who labeled them and the head each is at to the job summary, so
the Approve step shows what a release contains.

A label can be removed and set again, so only the last labeling event counts. A
pull request is often pushed to after it was labeled, so the check is on who
made the decision rather than on the code having stood still.

A hotfix skips the check, since it merges nothing. There is no flag to skip it
otherwise: removing the label, or adding the person to the list, is quicker than
a rerun and leaves the next release correct too.
Pin the actions to the commit behind the tag already in use, so this is not a
version bump. Check out without persisting credentials where nothing is pushed,
and give the jobs that need no token an empty permissions block.

Permissions are per job rather than workflow-wide on purpose: a called workflow
can only lower the permissions it is given, so a workflow-level contents:read
would remove the id-token:write that gha-docker and gha-helm need to
authenticate against GCP.
@vpaturet
vpaturet force-pushed the ci/harden-release-workflow branch from c574b27 to 80c415d Compare September 15, 2026 12:06
@vpaturet vpaturet changed the title ci: least-privilege cleanup of the Entur workflows Least-privilege cleanup of the Entur workflows Sep 15, 2026
…eout

custom-release.py runs every git command through execute(), which sets a 20 s
subprocess timeout, and the script catches nothing, so a slow command ends the
release with a traceback. Pushing into the empty staging repository this branch
introduces sends the whole history rather than the few new commits, which is
over that limit: measured at 23.6 s with two pack threads and 25.7 s with four,
on a machine with faster cores than a runner has.

Give the staging repository this checkout's objects through an alternates file,
and a refs/heads/main starting at the current release, so the push stays the
small incremental push it was against GitHub. Measured on the same machine:
0.02 s to set up, then 1.44 s for the branch and 0.87 s for the tag.

The object directory is resolved with --git-path rather than --absolute-git-dir
so that the step can also be rehearsed from a worktree.
'gh api --paginate' runs '--jq' once per page rather than over the whole
response. That is not visible today only because --paginate asks for 100 items
a page and every labeled pull request has a shorter timeline than that; on
issue 4000, whose timeline is 254 events, the same call returns three results.

Past 100 events LABELER therefore became one line per page, blank for every
page without a labeling event. Since a command substitution strips only the
trailing newlines, a label set late in a long timeline left a leading blank
line, and 'grep' reads a multi-line pattern as a list of patterns - the first
of which was a single space. The allow-list then matched unconditionally and
the gate passed anyone.

Collect the pages with --slurp before filtering, which cannot be combined with
--jq and so goes through a pipe, and compare the login against each allow-list
entry in full rather than matching it as a pattern. Checked against the three
labeled pull requests: the step still passes and prints the same table, it
fails with the expected annotation when the labeler is removed from the list,
and it now rejects the multi-line value the previous test accepted.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant