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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to this project are documented in this file.

## [0.0.31] - 2026-09-13

### Added

- Added a least-privilege GitHub App authentication pattern for workflows that resolve private sibling repositories, including short-lived read-only tokens, process-scoped Git configuration, exact repository selection, and fork pull-request and self-hosted-runner security boundaries.

## [0.0.30] - 2026-09-12

### Changed
Expand Down
33 changes: 33 additions & 0 deletions Guidelines/CICD.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,39 @@ Use Swift for new repository-owned executable scripts in Swift-focused applicati

Fall back to Python or a POSIX shell script only when the required behavior cannot be implemented with the repository's supported Swift toolchain and Foundation APIs. Document the exception in durable repository documentation in the same change, including the missing Swift capability, exact script and task scope, runtime and dependency requirements, security and maintenance impact, validation method, and condition for revisiting or removing the exception. Keep the fallback narrow; an existing non-Swift script does not authorize another one. The central `Scripts/swift_format.sh` command wrapper is the retained documented exception for invoking Xcode's `swift-format` modes.

## Private repository dependencies

The workflow repository's `GITHUB_TOKEN` does not grant access to private dependencies in sibling repositories. When Swift Package Manager or another build tool must clone private ThatFactory repositories, use a GitHub App installed on every required dependency repository. The app does not need access to the workflow repository unless that repository is also an intended token target. Grant the app only read access to repository contents, mint a short-lived installation token with `actions/create-github-app-token`, and list the exact dependency repositories in the action's `repositories` input. Do not use a personal access token, a long-lived machine credential, or an organization-wide token when the GitHub App can provide the required scope.

Expose the installation token only to steps that resolve or build the private dependencies. Supply HTTPS authentication through Git's process-level `GIT_CONFIG_COUNT`, `GIT_CONFIG_KEY_0`, and `GIT_CONFIG_VALUE_0` environment variables so the credential is not persisted in repository or global Git configuration. Keep the existing dependency URLs as `https://github.com/<owner>/<repository>` URLs. For example:

```yaml
- name: Create private dependency token
id: private-dependencies
uses: actions/create-github-app-token@v3
with:
client-id: ${{ vars.PRIVATE_DEPENDENCIES_APP_CLIENT_ID }}
private-key: ${{ secrets.PRIVATE_DEPENDENCIES_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: |
first-private-package
second-private-package
permission-contents: read

- name: Test
env:
GIT_CONFIG_COUNT: 1
GIT_CONFIG_KEY_0: url.https://x-access-token:${{ steps.private-dependencies.outputs.token }}@github.com/.insteadOf
GIT_CONFIG_VALUE_0: https://github.com/
run: swift test
```

The GitHub App's installation and repository selection are part of the security boundary. Consumer documentation must name the app variable and secret, list the private repositories the workflow requires, record the required `Contents: read` permission, and identify the jobs or steps that receive the token. Keep pull-request and protected-branch workflows consistent unless a documented trust boundary requires otherwise. Because `GIT_CONFIG_*` values are ordinary inherited environment variables, treat the credential-bearing resolve or build step and its complete subprocess tree as privileged. Tests, build scripts, SwiftPM plugins, and other code executed beneath that step must be trusted to receive read access to every repository in the token scope.

Repository secrets are unavailable to workflows triggered by pull requests from forks. A repository that accepts fork-originated or otherwise untrusted pull requests must keep a secretless validation path or deliberately skip private-dependency jobs with an explicit, documented condition. Untrusted code must not execute on a persistent self-hosted runner that is later reused for credential-bearing work. Use an isolated disposable or ephemeral self-hosted runner, an appropriate GitHub-hosted runner where possible, or a separate runner pool or host that never subsequently receives secrets; otherwise skip the untrusted validation.

This trust rule is event-independent. Do not combine credentials with code that is not trusted at that privilege level under `pull_request`, `pull_request_target`, `issue_comment`, `workflow_run`, or another trigger. On self-hosted runners, do not expose the token to unrelated steps, caches, artifacts, logs, or persistent configuration; retain the action's default post-job token revocation.

## `ci-pr.yml`

Projects using GitHub Actions should keep pull-request validation in `.github/workflows/ci-pr.yml`, triggered by `pull_request` events for `opened`, `synchronize`, and `reopened`.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ From the consumer repository root, install a tagged release:
git subtree add \
--prefix=AgentGuidelines \
https://github.com/thatfactory/agent-guidelines.git \
0.0.30 \
0.0.31 \
--squash
```

Expand Down Expand Up @@ -147,7 +147,7 @@ Review the target release's changelog, then pull it deliberately:
git subtree pull \
--prefix=AgentGuidelines \
https://github.com/thatfactory/agent-guidelines.git \
0.0.30 \
0.0.31 \
--squash
```

Expand Down
10 changes: 10 additions & 0 deletions Scripts/validate_guidelines.swift
Original file line number Diff line number Diff line change
Expand Up @@ -579,11 +579,21 @@ func validateExternalDependencyPolicy(_ errors: inout [String]) {
if let contents = readText(cicdGuideline, errors: &errors) {
let required = [
"## Tooling and automation": "CI/CD tooling policy section",
"## Private repository dependencies": "private repository dependency authentication section",
"Fastlane is forbidden": "forbidden delivery tooling",
"xcode-cloud-mcp": "first-party Xcode Cloud tooling",
"app-store-connect-mcp": "first-party App Store tooling",
"required behavior cannot be implemented": "non-Swift capability-gap threshold",
"missing Swift capability": "documented non-Swift exception",
"actions/create-github-app-token@v3": "short-lived GitHub App token workflow",
"client-id:": "current GitHub App client identifier input",
"permission-contents: read": "read-only private dependency permission",
"GIT_CONFIG_KEY_0": "process-level Git authentication",
"GIT_CONFIG_VALUE_0: https://github.com/": "GitHub HTTPS rewrite source",
"complete subprocess tree as privileged": "credential-bearing subprocess trust boundary",
"isolated disposable or ephemeral self-hosted runner": "untrusted-code runner isolation",
"This trust rule is event-independent": "event-independent credential boundary",
"pull_request_target": "untrusted pull-request credential boundary",
]
for (value, description) in required where !contents.contains(value) {
errors.append("Guidelines/CICD.md: missing \(description): '\(value)'")
Expand Down
21 changes: 21 additions & 0 deletions Tests/run_tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,27 @@ let tests: [(String, () throws -> Void)] = [
}
}
),
(
"repository validator rejects private dependency authentication drift",
{
try withTemporaryDirectory { temporary in
let fixture = temporary.appendingPathComponent("repository")
try copyRepositoryFixture(to: fixture)
let guideline = fixture.appendingPathComponent("Guidelines/CICD.md")
var contents = try String(contentsOf: guideline, encoding: .utf8)
contents = contents.replacingOccurrences(
of: "isolated disposable or ephemeral self-hosted runner",
with: "self-hosted runner")
try write(contents, to: guideline)
let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path])
try require(!result.succeeded, "private dependency authentication drift unexpectedly passed")
try require(
result.output.contains("missing untrusted-code runner isolation"),
result.output
)
}
}
),
(
"repository validator rejects gitignore template drift",
{
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.0.30
0.0.31