Skip to content

Commit 49111dd

Browse files
authored
Initial commit
0 parents  commit 49111dd

37 files changed

+6957
-0
lines changed

.commitlintrc.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
extends:
2+
- '@commitlint/config-conventional'
3+
rules:
4+
type-enum: [2, 'always', [
5+
'build',
6+
'docs',
7+
'feat',
8+
'fix',
9+
'perf',
10+
'refactor',
11+
'revert',
12+
'test'
13+
]]
14+
scope-empty: [0, 'never']

.editorconfig

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# EditorConfig is awesome: https://editorconfig.org
2+
3+
# top-most EditorConfig file
4+
root = true
5+
6+
[{*,*.{js, json, md, mjs, yaml, yml}}]
7+
indent_style = space
8+
end_of_line = lf
9+
insert_final_newline = true
10+
trim_trailing_whitespace = true
11+
charset = utf-8
12+
indent_size = 2
13+
14+
[docs/**/*.md]
15+
# turn off max line length on docs markdown files because line breaks have meaning in the docs when rendered by 11ty
16+
max_line_length = off
17+
18+
[*.js]
19+
max_line_length = 88

.github/dependabot.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# To get started with Dependabot version updates, you'll need to specify which
2+
# package ecosystems to update and where the package manifests are located.
3+
# Please see the documentation for all configuration options:
4+
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5+
6+
version: 2
7+
updates:
8+
- package-ecosystem: "github-actions"
9+
directory: "/"
10+
schedule:
11+
interval: "weekly"
12+
commit-message:
13+
prefix: "build"
14+
15+
- package-ecosystem: "npm"
16+
directory: "/"
17+
schedule:
18+
interval: "weekly"
19+
commit-message:
20+
prefix: "build"

.github/workflows/codeql.yml

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# For most projects, this workflow file will not need changing; you simply need
2+
# to commit it to your repository.
3+
#
4+
# You may wish to alter this file to override the set of languages analyzed,
5+
# or to provide custom queries or build logic.
6+
#
7+
# ******** NOTE ********
8+
# We have attempted to detect the languages in your repository. Please check
9+
# the `language` matrix defined below to confirm you have the correct set of
10+
# supported CodeQL languages.
11+
#
12+
name: "CodeQL Advanced"
13+
14+
on:
15+
workflow_dispatch:
16+
push:
17+
branches: [ "main", "sandbox" ]
18+
pull_request:
19+
branches: [ "main", "sandbox" ]
20+
schedule:
21+
- cron: '41 9 * * 5'
22+
23+
jobs:
24+
analyze:
25+
name: Analyze (${{ matrix.language }})
26+
# Runner size impacts CodeQL analysis time. To learn more, please see:
27+
# - https://gh.io/recommended-hardware-resources-for-running-codeql
28+
# - https://gh.io/supported-runners-and-hardware-resources
29+
# - https://gh.io/using-larger-runners (GitHub.com only)
30+
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
31+
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
32+
permissions:
33+
# required for all workflows
34+
security-events: write
35+
36+
# required to fetch internal or private CodeQL packs
37+
packages: read
38+
39+
# only required for workflows in private repositories
40+
actions: read
41+
contents: read
42+
43+
strategy:
44+
fail-fast: false
45+
matrix:
46+
include:
47+
- language: actions
48+
build-mode: none
49+
- language: javascript-typescript
50+
build-mode: none
51+
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
52+
# Use `c-cpp` to analyze code written in C, C++ or both
53+
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
54+
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
55+
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
56+
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
57+
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
58+
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
59+
steps:
60+
- name: Checkout repository
61+
uses: actions/checkout@v5
62+
63+
# Add any setup steps before running the `github/codeql-action/init` action.
64+
# This includes steps like installing compilers or runtimes (`actions/setup-node`
65+
# or others). This is typically only required for manual builds.
66+
# - name: Setup runtime (example)
67+
# uses: actions/setup-example@v1
68+
69+
# Initializes the CodeQL tools for scanning.
70+
- name: Initialize CodeQL
71+
uses: github/codeql-action/init@v3
72+
with:
73+
languages: ${{ matrix.language }}
74+
build-mode: ${{ matrix.build-mode }}
75+
# If you wish to specify custom queries, you can do so here or in a config file.
76+
# By default, queries listed here will override any specified in a config file.
77+
# Prefix the list here with "+" to use these queries and those in the config file.
78+
79+
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
80+
# queries: security-extended,security-and-quality
81+
82+
# If the analyze step fails for one of the languages you are analyzing with
83+
# "We were unable to automatically build your code", modify the matrix above
84+
# to set the build mode to "manual" for that language. Then modify this step
85+
# to build your code.
86+
# ℹ️ Command-line programs to run using the OS shell.
87+
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
88+
- if: matrix.build-mode == 'manual'
89+
shell: bash
90+
run: |
91+
echo 'If you are using a "manual" build mode for one or more of the' \
92+
'languages you are analyzing, replace this with the commands to build' \
93+
'your code, for example:'
94+
echo ' make bootstrap'
95+
echo ' make release'
96+
exit 1
97+
98+
- name: Perform CodeQL Analysis
99+
uses: github/codeql-action/analyze@v3
100+
with:
101+
category: "/language:${{matrix.language}}"
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# GitHub Actions Workflow: `fast-forward-pr-merge-init.yml`
2+
3+
This document explains the purpose, logic, and security rationale behind the [`.github/workflows/fast-forward-pr-merge-init.yml`][1] workflow in this repository.
4+
5+
## Overview
6+
7+
This workflow is the **first phase** of a two-phase secure PR merge process designed to control and secure the merging of pull requests (PRs) via the addition of a special `fast-forward` label to a pull request. It ensures that only users with sufficient repository permissions (`write` or `admin`) can trigger a merge, and provides clear feedback to users who lack the required permissions.
8+
9+
If the user passes these checks, a second workflow, [`fast-forward-pr-merge-privileged.yml`][2], is triggered to perform the actual fast-forward merge and post the final result.
10+
11+
It is especially important when accepting contributions from external users via a forked repository, which is how most open source contributions are made.
12+
13+
## When Does This Workflow Run?
14+
15+
The workflow is triggered when a pull request receives the `fast-forward` label (`pull_request` event, type `labeled`).
16+
17+
## What Does the Workflow Do?
18+
19+
### 1. Checks User Permissions
20+
21+
- The workflow determines the GitHub username of the user who added the label.
22+
- It uses the GitHub CLI (`gh api`) to fetch the user's permission level on the repository (e.g., `read`, `triage`, `write`, `maintain`, `admin`).
23+
- The permission is stored for use in later steps.
24+
25+
### 2. Handles Insufficient Permissions
26+
27+
- If the user **DOES NOT** have `write` or `admin` permissions:
28+
- The workflow fails with a clear error message, preventing any further merge automation.
29+
- The privileged workflow (second phase) will post a comment on the PR stating that the user does not have permission to merge the PR and should contact a member of the [API Standards Team][3] for assistance.
30+
31+
### 3. Event Payload Publishing
32+
33+
- If the user **DOES** have `write` or `admin` permissions:
34+
- The workflow saves the full event payload (the GitHub event data) as an artifact for use with the second phase of the workflow, but it's also useful for auditing or debugging purposes.
35+
36+
## Why Is This Workflow Needed?
37+
38+
- **True Fast-Forward Merges:** GitHub's "Rebase and merge" option is not a true fast-forward merge. Instead, it rewrites commit SHAs and the process strips any commit signatures, which can break commit verification and audit trails. This workflow enables a genuine fast-forward merge, preserving original commit SHAs and signatures (at least until GitHub support a true fast-forward merge).
39+
- See: [GitHub Docs – About merge methods on GitHub][4]
40+
- See: [GitHub Community - Feature Request: Only allow for --ff merges for PRs][5]
41+
- See: [GitHub Community – GPG signature lost when merging a PR][6]
42+
43+
- **Preserves Conventional Commit History:** True fast-forward merges retain the original commits and their message structure intact, which is especially valuable for our adoption of [Conventional Commits][7]. Unlike GitHub's `squash-merges` which combine all changes into a single commit with limited control over the structure of the final commit message (making it difficult to ensure it adheres to the conventional commit standard). This enables automated changelog generation, better traceability of features and fixes, and more meaningful project history.
44+
45+
## Why is the Workflow Split into Two Phases?
46+
47+
Splitting the merge process into two distinct workflows is a best practice for securing GitHub Actions, especially when privileged operations (like merging to a protected branch) are involved. This approach is recommended by GitHub Security Lab and is designed to mitigate several classes of vulnerabilities:
48+
49+
### Security Boundaries
50+
51+
By separating the unprivileged (label-triggered) phase from the privileged (merge-executing) phase, you create a strong security boundary. The first workflow runs with minimal permissions and only performs checks and artifact publishing. The second, privileged workflow (triggered by `workflow_run`) runs with elevated permissions and only after all checks have passed.
52+
53+
### Prevents Privilege Escalation
54+
55+
If a single workflow both checked permissions and performed the merge, an attacker could potentially exploit timing or event confusion such as **Time Of Check to Time Of Use** ([TOCTOU][8]) attacks to escalate privileges or inject malicious code between steps. By splitting the workflows, the privileged phase only runs after the unprivileged phase has completed and its outputs (artifacts) are validated.
56+
57+
### Mitigates Artifact Poisoning
58+
59+
Artifacts passed from the unprivileged to the privileged workflow are treated as untrusted. This separation allows you to validate and sanitise any data before it is used in a privileged context, reducing the risk of artifact poisoning attacks.
60+
61+
### Reduces Risk from Untrusted Triggers
62+
63+
Triggers like `issue_comment` or `pull_request_target` can be abused by attackers to execute workflows with elevated permissions. By using a label gate and a two-phase approach, you ensure that only trusted actors (with `write` or `admin` permissions) can initiate the privileged merge, and that the code being merged is exactly what was reviewed and approved.
64+
65+
### Aligns with GitHub Security Guidance
66+
67+
GitHub’s own security research recommends splitting workflows into unprivileged and privileged components, using `workflow_run` as a secure handoff point. This pattern is now widely adopted in the open source community to prevent supply chain attacks and workflow abuse.
68+
69+
[1]: ./.github/workflows/fast-forward-pr-merge-init.yml
70+
[2]: ./fast-forward-pr-merge-privileged.md
71+
[3]: https://github.com/orgs/ukhsa-collaboration/teams/api-standards-team
72+
[4]: https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/about-merge-methods-on-github#rebasing-and-merging-your-commits
73+
[5]: https://github.com/orgs/community/discussions/4618
74+
[6]: https://github.com/orgs/community/discussions/10410
75+
[7]: https://www.conventionalcommits.org/
76+
[8]: https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: Fast Forward PR Merge Initialise
2+
3+
on:
4+
pull_request:
5+
types: [labeled]
6+
7+
branches:
8+
- main
9+
10+
jobs:
11+
initialise-workflow:
12+
if: |
13+
github.event.label.name == 'fast-forward' &&
14+
!github.event.pull_request.closed_at &&
15+
github.event.pull_request.state == 'open'
16+
17+
name: Initialise Workflow
18+
runs-on: ubuntu-latest
19+
permissions:
20+
actions: read
21+
22+
env:
23+
USERNAME: ${{ github.actor }}
24+
25+
steps:
26+
27+
- name: Get permissions
28+
id: permission
29+
env:
30+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
31+
GH_API_VERSION: '2022-11-28'
32+
run: |
33+
echo "Fetching permissions for user: $USERNAME"
34+
RESPONSE=$(gh api \
35+
-H "Accept: application/vnd.github+json" \
36+
-H "X-GitHub-Api-Version: $GH_API_VERSION" \
37+
repos/$GITHUB_REPOSITORY/collaborators/$USERNAME/permission | jq -r .permission)
38+
echo "Permission: $RESPONSE"
39+
echo "permission=$RESPONSE" >> $GITHUB_OUTPUT
40+
41+
- name: Set failure message
42+
uses: actions/github-script@v8
43+
if: ${{ steps.permission.outputs.permission != 'write' && steps.permission.outputs.permission != 'admin' }}
44+
with:
45+
script: core.setFailed("You do not have permission to merge this PR. Please contact a member of ${{ github.server_url }}/orgs/${{ github.repository_owner }}/teams/api-standards-team for assistance.");
46+
47+
- name: Upload Event Artifact
48+
if: ${{ always() }}
49+
uses: actions/upload-artifact@v4
50+
with:
51+
name: event-artifact
52+
path: ${{ github.event_path }}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# GitHub Actions Workflow: `fast-forward-pr-merge-privileged.yml`
2+
3+
This document explains the purpose, logic, and security rationale behind the [`.github/workflows/fast-forward-pr-merge-privileged.yml`][1] workflow in this repository.
4+
5+
## Purpose and Overview
6+
7+
This workflow is the **second phase** of a two-phase pull request (PR) merge process. It works in tandem with the [`fast-forward-pr-merge-init.yml`][2] workflow to allow privileged users to trigger a fast-forward merge of a PR by adding the `fast-forward` label to an open pull request.
8+
9+
The workflow ensures that only users with `write` or `admin` permissions can perform this action, and that the PR is in a clean, mergeable state. It provides clear feedback to users and maintains a full audit trail of the event and actions taken.
10+
11+
## How Does It Work?
12+
13+
### 1. Triggering
14+
15+
- The workflow is triggered by a `workflow_run` event, specifically when the `Fast Forward PR Merge Initialise` workflow completes.
16+
- It only proceeds if the initial workflow succeeded and the event was an `pull_request` being labelled with `fast-forward` and is still open.
17+
18+
### 2. Importing and Validating Event Data
19+
20+
- Downloads the event artifact created by the initial workflow, which contains the full GitHub event payload (including the PR and label details).
21+
22+
### 3. On Failure
23+
24+
- If the initial workflow failed, this job posts a failure comment to the PR, sets a failure status, and removes the `fast-forward` label.
25+
26+
### 4. On Success and Permission Check
27+
28+
- If the initial workflow succeeded and the PR is still open and labelled correctly:
29+
- Checks the permissions of the user who triggered the label.
30+
- If the user does **not** have `write` or `admin` permissions at the time of merge, posts a failure comment and removes the label.
31+
32+
### 5. Fast-Forward Merging
33+
34+
- If all checks pass and the PR is in a `clean` (mergeable) state:
35+
- Checks out the incoming PR branch and the base branch.
36+
- Rebases the base branch onto the PR branch (fast-forward merge).
37+
- Pushes the updated base branch to GitHub.
38+
- If the PR is not mergeable (e.g., conflicts), it does not attempt the merge and posts a failure comment.
39+
- After any attempt, the `fast-forward` label is removed from the PR.
40+
41+
### 6. Feedback and Audit Trail
42+
43+
- Posts a comment on the PR indicating success or failure, with a link to the workflow run for details.
44+
- Maintains a full audit trail by saving event data and merge results.
45+
46+
## Why Is This Workflow Needed?
47+
48+
- **True Fast-Forward Merges:** GitHub's "Rebase and merge" option is not a true fast-forward merge. Instead, it rewrites commit SHAs and the process strips any commit signatures, which can break commit verification and audit trails. This workflow enables a genuine fast-forward merge, preserving original commit SHAs and signatures (at least until GitHub supports a true fast-forward merge).
49+
- See: [GitHub Docs – About merge methods on GitHub][3]
50+
- See: [GitHub Community - Feature Request: Only allow for --ff merges for PRs][4]
51+
- See: [GitHub Community – GPG signature lost when merging a PR][5]
52+
53+
- **Preserves Conventional Commit History:** True fast-forward merges retain the original commits and their message structure intact, which is especially valuable for our adoption of [Conventional Commits][6]. Unlike GitHub's `squash-merges` which combine all changes into a single commit with limited control over the structure of the final commit message (making it difficult to ensure it adheres to the conventional commit standard). This enables automated changelog generation, better traceability of features and fixes, and more meaningful project history.
54+
55+
[1]: ./fast-forward-pr-merge-privileged.yml
56+
[2]: ./fast-forward-pr-merge-init.md
57+
[3]: https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/about-merge-methods-on-github#rebasing-and-merging-your-commits
58+
[4]: https://github.com/orgs/community/discussions/4618
59+
[5]: https://github.com/orgs/community/discussions/10410
60+
[6]: https://www.conventionalcommits.org/

0 commit comments

Comments
 (0)