Author SHA1 Message Date
TirsvadandClaude Sonnet 5.5 05a7159c18 Ask for missing credentials and create the project's own .env
.env becomes optional. A credential it does not provide (an absent file, an
absent or empty key) is asked, without echo: GITEA_TOKEN at the start,
GITHUB_PAT and GITHUB_USER once GitHub is chosen. An invalid value is asked
again and never shown; when input ends the run stops before any request.
Asked tokens are registered for redaction at once.

After the local project exists the script asks (default no) whether to
create a .env in it. On a yes it holds only the needed keys, is created
private (mode 600) from the start, is excluded from git through
.git/info/exclude (no tracked file changes), is never replaced without a
second yes and is never written when git tracks it. The summary names the
keys, never the values.

New library files credentials.sh and envfile.sh; README, .env.example and
the security decisions updated; tests cover every case.

Task: MIL-005#1
Task: MIL-005#2
Task: MIL-005#3
Task: MIL-005#4
Task: MIL-005#5
Closes #35
Closes #36
Closes #37
Closes #38
Closes #39

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 13:29:01 +08:00
Tirsvad 3804ef7556 Merge pull request 'Add the Design Class Diagrams and plan MIL-005 (credentials asked, project .env)' (#40) from dcd-uc-001 into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Failing after 4s
Reviewed-on: #40
2026-10-06 07:01:40 +02:00
TirsvadandClaude Sonnet 5.5 7d202a34dc Resolve pending commit links for the MIL-005 plan
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 12:47:33 +08:00
TirsvadandClaude Sonnet 5.5 ded26a658c Plan MIL-005: ask for missing credentials and create the project .env
Add objective 9 to the Business Case (and amend objective 6 and success
criterion 1: a token may be written only to the new project's .env after a
yes), US-001.05, UC-001 extensions 2b, 9c and 9d, with the SSD, OC, SD,
DM-001, DM-002 and dictionary in step. Add the classes CredentialCollector,
EnvFileWriter and EnvFile to DCD-001 and DCD-002, and both ends'
multiplicities to every association. Restore three lines of SD-001 damaged
by an earlier edit.

Add milestone MIL-005 (9 Go/No-Go criteria, 5 tasks) and its phase in the
Project Plan. Accept the planning set and the two DCDs; reviews recorded in
RC-020 and RC-021.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 12:47:26 +08:00
TirsvadandClaude Sonnet 5.5 4c9a1af719 Resolve pending commit links for the DCDs
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 12:35:47 +08:00
TirsvadandClaude Sonnet 5.5 f4d611b77c Add the Design Class Diagrams DCD-001 and DCD-002, align SD-001
- DCD-001 (UC-001): design classes refining DM-001, with class table,
  method traceability to OC-001 and SD-001, patterns and dependency check,
  and the mapping of each class to src/lib.
- DCD-002: the consolidated project-level model, created from DCD-001.
- SD-001: messages aligned with the DCD method signatures; cites DCD-001.
- Dictionary, registry and traceability matrix updated.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 12:35:45 +08:00
Tirsvad cf265e8f66 Merge pull request 'MIL-004: project details preset in config.env (with the e2e fixes and the plan)' (#34) from mil-004-configurable-details into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 5s
Reviewed-on: #34
2026-10-05 18:28:47 +02:00
TirsvadandClaude Sonnet 5.5 55d9ca6eee Add a section banner to .env.example
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 00:27:00 +08:00
TirsvadandClaude Sonnet 5.5 6b1b9c6af4 Ignore config.env
The file holds the Maintainer's own addresses and project details, so it
stays out of the repository like .env.

Refs #31

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 00:25:48 +08:00
Tirsvad c21a3c50f0 Merge pull request 'MIL-003: local project, framework submodule, hooks, templates and README' (#26) from mil-003-scaffold-and-release into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 4s
Reviewed-on: #26
2026-10-05 09:55:13 +02:00
Tirsvad 5584ddf397 Merge pull request 'MIL-002: GitHub and Gitea steps (dry run, repositories, push mirror) and split into library files' (#25) from mil-002-repositories-and-mirror into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 4s
Reviewed-on: #25
2026-10-05 09:33:06 +02:00
31 changed files with 1651 additions and 152 deletions
+8 -1
View File
@@ -1,6 +1,9 @@
# RepoFoundry credentials. Placeholders only: never put a real value in this
# file or commit one.
#
# Everything in this file is optional: a credential that is missing here is
# asked for when the script runs (the token is not echoed).
#
# Copy this file to .env, fill in the values and keep it private
# (chmod 600 .env on Linux and macOS). .env is ignored by git. The file is
# read as plain KEY=VALUE lines and never executed. Values may be wrapped in
@@ -12,11 +15,15 @@
# chosen owner and to push to the new one. Prefer a fine-grained token.
GITHUB_PAT=
########################################
# Secrets for framework
########################################
# GitHub account the token belongs to. It identifies who authenticates; it is
# only a default suggestion for the owner prompt, because the repository can
# belong to an organization.
GITHUB_USER=
# Gitea access token (required). It needs permission to create repositories
# Gitea access token. It needs permission to create repositories
# for the chosen owner and to manage the repository's push mirror.
GITEA_TOKEN=
+1
View File
@@ -187,3 +187,4 @@ repofoundry.*/
!src/lib
config.env
+34 -4
View File
@@ -129,14 +129,40 @@ src/create-project.sh --apply # asks only "Create these now (y/n) [n]"
| Key | Meaning |
| --- | --- |
| `GITEA_TOKEN` | Gitea access token (required) |
| `GITEA_TOKEN` | Gitea access token |
| `GITHUB_PAT` | GitHub personal access token (only when you choose GitHub) |
| `GITHUB_USER` | the GitHub account the token belongs to; only a default for the owner prompt |
| `GITHUB_USER` | the GitHub account the token belongs to (only when you choose GitHub) |
`.env` is ignored by git. The script warns if it is readable by other users or
not ignored by git. See [Token permissions](#token-permissions) for what each
token needs.
`.env` is optional, and so is each key in it. A credential that is not
provided (the file is missing, the key is absent or its value is empty) is
asked for: the Gitea token at the start, the GitHub token and account name once
you choose GitHub. A token is read without echo and checked like one read from
`.env`; a refused value is asked again and never shown. If input ends before a
valid value is entered, the run stops before any request to a host.
### The project's own `.env`
When the project exists, the script asks whether to create a `.env` in it
(default no). On a yes the file holds only the credentials the project needs:
`GITEA_TOKEN`, and `GITHUB_PAT` and `GITHUB_USER` when you chose GitHub, as read
from your `.env` or typed.
- The file is created readable by you only (mode 600), never readable by
others even for a moment, and is never written by anything else.
- Git ignores it: the script adds `.env` to `.git/info/exclude` of the new
project. No tracked file changes and nothing is committed.
- An existing `.env` in the project is never replaced without a second yes, and
a `.env` that git already tracks is never written.
- The summary names the keys, never the values.
This is the one place the script writes a token to disk. It is plain text: keep
the project directory private, do not copy the file around, and say no if you
do not need it. Tokens are written nowhere else.
## Usage
```bash
@@ -264,7 +290,9 @@ script stops before it creates anything when a preflight check is refused.
## Security decisions
- **Tokens never appear** in output, logs, remote URLs, `.git/config`,
`.gitmodules`, command lines or leftover files. They go to `curl` through a
`.gitmodules`, command lines or leftover files, and are written to disk only
in the new project's own `.env`, after a yes (see
[The project's own `.env`](#the-projects-own-env)). They go to `curl` through a
private configuration file that is removed right after the request, and to
`git` (HTTPS fetch only) through a `GIT_ASKPASS` helper and the environment
of that one command. Output is filtered, so even a server message that echoes
@@ -368,11 +396,13 @@ file. The files are loaded from that directory only, by a fixed path.
| `json.sh` | the little JSON the script reads and writes |
| `http.sh` | the one place that runs `curl`; tokens stay off the command line |
| `api.sh` | GitHub and Gitea API calls and reporting a refused call |
| `prompts.sh` | interactive questions with validation |
| `prompts.sh` | interactive questions with validation (secrets are read without echo) |
| `credentials.sh` | asking for a credential that `.env` does not provide |
| `project.sh` | the project details: asking for them and showing them |
| `hosts.sh` | names, links and remote addresses of the repositories |
| `preflight.sh` | read-only checks of both hosts |
| `steps.sh` | the outcome of each step and the final report |
| `envfile.sh` | the new project's own `.env`: created private, ignored by git, never replaced without a yes |
| `plan.sh` | printing what the script is about to do |
| `repositories.sh` | creating the GitHub and Gitea repositories |
| `mirror.sh` | the Gitea to GitHub push mirror |
+3 -2
View File
@@ -14,16 +14,17 @@ document of a type. `Primary File` may contain a glob (e.g.
| BC | Business Case | docs/business-case.md | 002 |
| SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 |
| PP | Project Plan | docs/project-plan.md | 002 |
| MIL | Milestone / Gateway | docs/milestones/*.md | 005 |
| MIL | Milestone / Gateway | docs/milestones/*.md | 006 |
| US | User Story | docs/user-stories.md | 002 |
| UC | Use Case | docs/uc-*/uc.md | 002 |
| SSD | System Sequence Diagram | docs/uc-*/ssd.md | 002 |
| OC | Operation Contract | docs/uc-*/oc.md | 002 |
| SD | Sequence Diagram | docs/uc-*/sd.md | 002 |
| DM | Domain Model | docs/domain-model.md | 003 |
| DCD | Design Class Diagram | docs/dcd.md | 003 |
| DICT | Domain Dictionary (PO and IT terms) | docs/dictionary.md | 002 |
| UCD | Use Case Diagram | docs/use-case-diagram.md | 002 |
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 020 |
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 022 |
| TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 |
## Languages
+11 -5
View File
@@ -9,8 +9,8 @@
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Optional GitHub; choosing GitHub applies the AGPL license to the Gitea repository<br>Cited UCD-001<br>Justified the qualitative cost-benefit; stakeholder roles replaced by interests; success criteria 2 and 3 reworded for optional GitHub<br>Added objective 7 (documentation) and its success criterion | [02875ae] |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Added objective 8 (project details preset in config.env), the matching scope item and success criterion 8 | [2a6bb8e] |
| 2026-10-05 | Deprecated | Jens Tirsvad Nielsen | S02 | Added objective 8 (project details preset in config.env), the matching scope item and success criterion 8 | [2a6bb8e] |
| 2026-10-06 | Accepted | Jens Tirsvad Nielsen | S02 | Added objective 9 (credentials asked, project .env created), scope items, success criterion 9 and a risk<br>Objective 6 and success criterion 1 now allow a token only in the new project's .env | [ded26a6] |
---
@@ -39,9 +39,10 @@ One repeatable, reviewed procedure gives every new project the same secure basel
3. When GitHub was chosen, configure the Gitea repository as a push mirror to GitHub (direction Gitea to GitHub).
4. Create the local project directory with an `origin` (Gitea) remote and, when GitHub was chosen, a `github` remote, neither containing credentials.
5. Add the SQA-QC-Framework as the `framework` submodule, install its skills and git hooks, and copy its templates, optionally enabling the plan gate.
6. Never print or persist a token, and never overwrite existing files or directories without consent.
6. Never print a token or put one in a URL, a remote or a log, write one to disk only in the new project's own `.env` and only after the Maintainer agrees, and never overwrite existing files or directories without consent.
7. Document installation, configuration, usage, security decisions and error handling in clear English for GitHub readers.
8. Let the Maintainer preset the project details in `config.env`, so that a detail that is set there is not asked again.
9. Ask for a credential that is not provided in `.env` (`GITEA_TOKEN`, `GITHUB_PAT`, `GITHUB_USER`) and, when the Maintainer agrees, create a `.env` file with the credentials the new project needs.
## Scope
@@ -52,6 +53,7 @@ One repeatable, reviewed procedure gives every new project the same secure basel
- Prompts for name, description, visibility and owner on each chosen host, and whether to use GitHub (which also applies the AGPL license). Each of these details may be set in `config.env` instead and is then not asked.
- Checks for required tools (`git`, `curl`, optional `jq`) before any change.
- A check that the project name is not already taken on GitHub.
- Asking for a credential that `.env` does not provide, and creating the new project's own `.env` (owner-only, ignored by git, never overwritten without a yes).
- Partial-failure reporting with a documented way to continue.
- Documentation of the SSH prerequisite for the submodule (Gitea SSH on port `10022`).
@@ -61,6 +63,7 @@ One repeatable, reviewed procedure gives every new project the same secure basel
- Managing repositories after creation (branch protection, webhooks, teams, CI).
- Hosts other than GitHub and the configured Gitea instance.
- Creating or rotating tokens and SSH keys.
- Storing a credential anywhere but the new project's `.env` (no password manager, keychain or encryption).
- Making the first commit or opening a pull request.
## Expected Benefits
@@ -83,7 +86,7 @@ Supports developing on self-hosted Gitea while publishing to GitHub, and adoptin
| # | Criterion | Target | Measure |
| --- | --- | --- | --- |
| 1 | Credential exposure | 0 occurrences of a token in output, saved remote URLs, config files or leftover temp files | Test run with log review; `git config --get-regexp remote` inspected |
| 1 | Credential exposure | 0 occurrences of a token in output, saved remote URLs, tracked files, config files or leftover temp files; a token is written only to the new project's `.env` (owner-only, ignored by git) and only after a yes | Test run with log review; `git config --get-regexp remote` inspected; every file of the new project searched for the tokens |
| 2 | Repository ownership | Each repository created is under the owner chosen at the prompt for that host, never silently under `GITHUB_USER` | Test run with a user owner and with an organization owner |
| 3 | Mirror direction | When GitHub is chosen, Gitea is the source and GitHub the target; a push to `origin` appears on GitHub | Push a test commit and compare |
| 4 | Partial failure | When one host fails, the output lists what was created and the command to continue | Forced failure test (invalid token for one host) |
@@ -91,6 +94,7 @@ Supports developing on self-hosted Gitea while publishing to GitHub, and adoptin
| 6 | Lint | `shellcheck` reports no errors on `create-project.sh` | `shellcheck create-project.sh` |
| 7 | Documentation | `README.md` covers installation, configuration, usage, security decisions, error handling and stakeholders | Review by S02 against MIL-003 Go/No-Go criterion 6 |
| 8 | Preset details | A project detail set in `config.env` is never asked; an invalid one stops the run before any request and names the key | Tests with each key set, absent, empty and invalid |
| 9 | Credentials asked and kept | A credential missing from `.env` is asked (not echoed) instead of stopping the run; the new project's `.env` is created only after a yes, owner-only, ignored by git, holding only the keys the project needs, and an existing `.env` is never replaced without a yes | Tests: each credential present and missing, `.env` written, declined, existing, file mode, git exclusion, no token in output |
## Risks
@@ -100,6 +104,7 @@ Supports developing on self-hosted Gitea while publishing to GitHub, and adoptin
| GitHub PAT lacks permission to create repositories or to push | Creation or mirroring fails | Document the required scopes; check with a read-only API call first and stop with a clear message |
| Gitea stores the mirror credentials server-side | A Gitea admin could access the GitHub token | Document it; recommend a fine-grained PAT limited to the one repository where possible |
| SSH to Gitea port `10022` is not configured | Submodule add fails after repositories already exist | Check SSH reachability before creating anything; document the prerequisite |
| A token written to the new project's `.env` is plain text on disk and could be committed or copied by mistake | A leaked token gives access to the hosts | Ask first (default no), write only the keys the project needs, mode owner-only, exclude the file from git through `.git/info/exclude`, never overwrite an existing `.env` without a yes, never print the value, document the risk |
| Framework hook installer changes `core.hooksPath` | An existing hook setup is silently replaced | Inspect the current value first and ask for consent |
| Repository name conflicts on a host | Creation fails midway | Check availability on both hosts before creating either |
@@ -114,6 +119,7 @@ Supports developing on self-hosted Gitea while publishing to GitHub, and adoptin
- Bash only, with `git` and `curl` required and `jq` optional.
- `config.env` and `.env` are parsed, never `source`d.
- No `rm -rf`, and no token in any URL, log or remote.
- A token on disk only in the new project's `.env`, created by the script with the Maintainer's consent.
- The framework under `framework/` is not edited from this project.
## Cost–Benefit Assessment
@@ -140,5 +146,5 @@ Proceed — the procedure is small, well bounded and removes a repeated, securit
[SA-001]: ./stakeholder-analysis.md
[UCD-001]: ./use-case-diagram.md
[02875ae]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/02875aee5f2953473924074eea0056eb31af6b7a
[2a6bb8e]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/2a6bb8e8afadfe6ca4a621da30e44a372898ca62
[ded26a6]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/ded26a658c666bf29d84093cb352e3635e07719b
+337
View File
@@ -0,0 +1,337 @@
# Design Class Diagram
## Metadata
| Key | Value |
| --- | --- |
| ID | DCD-002 |
| CrossReference | [DCD-001], [DM-002], [UC-001], [OC-001], [SD-001], [DICT-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-06 | Deprecated | Jens Tirsvad Nielsen | S02 | Initial version, from DCD-001 (UC-001) | [f4d611b] |
| 2026-10-06 | Accepted | Jens Tirsvad Nielsen | S02 | Added CredentialCollector, EnvFileWriter and EnvFile (from DCD-001) | [ded26a6] |
---
## Purpose and Scope
The consolidated design model of the project. Use-case models are scoped views; when one changes, this model is checked and updated in the same change. It currently covers [UC-001] "Create a new project" ([DCD-001]), which it was created from, and refines the concepts of [DM-002]. Class and attribute names are the IT terms of [DICT-001]. Every method traces to a contract in [OC-001] or a message in [SD-001].
The classes are design classes of a Bash program: a class is a group of functions in `src/lib/` with its data held in the shared state arrays (see "Implementation Mapping").
## Diagram
```plantuml
@startuml
skinparam classAttributeIconSize 0
hide empty members
enum Visibility {
private
public
}
class ProjectCreator <<controller>> {
+startProjectCreation() : PromptSet
+provideProjectDetails(name : String, description : String, visibility : Visibility, giteaOwner : Owner, githubOwner : Owner [0..1], directory : Path, enablePlanGate : Boolean, writeEnvFile : Boolean) : Summary
}
class ConfigLoader {
+load(configFile : Path, envFile : Path) : Configuration
}
class CredentialCollector {
+collect(configuration : Configuration, kinds : String [1..3]) : Configuration
}
class EnvFileWriter {
+write(project : LocalProject, configuration : Configuration, hasGithub : Boolean) : EnvFile [0..1]
}
class ToolChecker {
+check(tools : String [1..*]) : ToolCheck
}
class Preflight {
+check(request : ProjectRequest) : PreflightResult
}
abstract class GitHost <<facade>> {
-name : String
-webAddress : String
-apiAddress : String
+verifyToken() : Boolean
+ownerAccepts(owner : Owner) : Boolean
+nameFree(name : String) : Boolean
}
class GiteaClient <<facade>> {
+GiteaClient(configuration : Configuration)
+hasLicense(key : String) : Boolean
+createRepository(request : ProjectRequest, license : String [0..1]) : GiteaRepository
+addPushMirror(source : GiteaRepository, target : GitHubRepository) : PushMirror
-requestSync(mirror : PushMirror) : void
}
class GitHubClient <<facade>> {
+GitHubClient(configuration : Configuration)
+createEmptyRepository(request : ProjectRequest) : GitHubRepository
}
class LocalProjectBuilder {
+build(directory : Path, source : GiteaRepository, target : GitHubRepository [0..1], sshPassed : Boolean) : LocalProject
}
class FrameworkInstaller {
+install(project : LocalProject, enablePlanGate : Boolean) : InstallResult
}
class SummaryReport {
+compose(request : ProjectRequest) : Summary
}
class Run {
-isApply : Boolean
}
class Configuration {
-giteaUrl : String
-giteaApiUrl : String
-githubWebUrl : String
-githubApiUrl : String
-giteaSshPort : Integer
-mirrorInterval : String
-frameworkRepo : String
-presetDetails : Map [0..1]
}
class Credential {
-kind : String
-value : String
}
class ToolCheck {
-hasGit : Boolean
-hasCurl : Boolean
-hasJq : Boolean
}
class PromptSet {
-prompts : String [1..*]
}
class ProjectRequest {
-name : String
-description : String
-visibility : Visibility
-directory : Path
-enablePlanGate : Boolean
}
class Owner {
-name : String
-kind : String
}
class PreflightResult {
-tokensWork : Boolean
-ownersAccept : Boolean
-nameIsFree : Boolean
-licenseIsOffered : Boolean
-sshPassed : Boolean
}
abstract class Repository {
-name : String
-description : String
-visibility : Visibility
-address : String
}
class GiteaRepository
class GitHubRepository
class LicenseFile {
-key : String
}
class PushMirror {
-interval : String
-syncOnCommit : Boolean
}
class LocalProject {
-directory : Path
}
class Remote {
-name : String
-address : String
}
class Submodule {
-name : String
-address : String
}
class HookSetup {
-areSkillsInstalled : Boolean
-areHooksInstalled : Boolean
-isPlanGateEnabled : Boolean
}
class EnvFile {
-address : Path
-keys : String [1..3]
}
class Template {
-name : String
-isCopied : Boolean
}
class InstallResult <<dto>>
class Summary {
-createdItems : String [0..*]
-skippedItems : String [0..*]
-nextSteps : String [0..*]
}
ProjectCreator ..> ConfigLoader : creates
ProjectCreator ..> ToolChecker : creates
ProjectCreator ..> CredentialCollector : creates
ProjectCreator ..> EnvFileWriter : creates [0..1]
ProjectCreator ..> Preflight : creates
ProjectCreator ..> GiteaClient : creates
ProjectCreator ..> GitHubClient : creates [0..1]
ProjectCreator ..> LocalProjectBuilder : creates
ProjectCreator ..> FrameworkInstaller : creates
ProjectCreator ..> SummaryReport : creates
Preflight ..> GiteaClient : asks
Preflight ..> GitHubClient : asks [0..1]
GitHost <|-- GiteaClient
GitHost <|-- GitHubClient
GitHost "1" --> "0..*" Owner : has
GiteaClient ..> Configuration
GitHubClient ..> Configuration
ProjectCreator "0..*" --> "1" Run
Run "1" *-- "1" Configuration
Run "1" *-- "1" ToolCheck
Run "1" *-- "0..1" ProjectRequest
Run "1" --> "1" PromptSet : returns
Configuration "1" *-- "1..3" Credential
ProjectRequest "0..*" --> "1" Owner : giteaOwner
ProjectRequest "0..*" --> "0..1" Owner : githubOwner
ProjectRequest "1" *-- "0..1" PreflightResult
ProjectRequest "1" --> "0..1" GiteaRepository : stored in
ProjectRequest "1" --> "0..1" GitHubRepository : also stored in
ProjectRequest "1" --> "0..1" LocalProject : working copy
Summary "0..*" --> "1" ProjectRequest : reports on
Repository <|-- GiteaRepository
Repository <|-- GitHubRepository
Repository "0..*" --> "1" Owner : owned by
GiteaRepository "1" *-- "0..1" LicenseFile
PushMirror "0..*" --> "1" GiteaRepository : source
PushMirror "0..*" --> "1" GitHubRepository : target
PushMirror "0..*" --> "1" Credential : authorised by
LocalProject "1" *-- "1..2" Remote
Remote "0..*" --> "1" Repository : points to
LocalProject "1" *-- "1" Submodule
LocalProject "1" *-- "1" HookSetup
LocalProject "1" *-- "0..*" Template
LocalProject "1" *-- "0..1" EnvFile
EnvFile "0..*" --> "1..3" Credential : copy of
InstallResult "0..*" --> "1" Submodule
InstallResult "0..*" --> "1" HookSetup
InstallResult "0..*" --> "0..*" Template
ProjectRequest "0..*" --> "1" Visibility
Repository "0..*" --> "1" Visibility
@enduml
```
## Class Table
| Class | Refines (Domain Model concept) | Responsibility | Attributes | Operations |
| --- | --- | --- | --- | --- |
| `ProjectCreator` | none (controller for the system operations of [OC-001]) | Receives the two system operations, sequences the steps and stops on the first failure. | none | `startProjectCreation`, `provideProjectDetails` |
| `ConfigLoader` | Configuration | Reads `config.env` and `.env` as plain text and validates every value, preset project details included. | none | `load` |
| `CredentialCollector` | none (system concept) | Asks, without echo, for a credential that `.env` does not provide and validates it like one read from `.env`. | none | `collect` |
| `EnvFileWriter` | Credentials File | Creates the project's own `.env` with the credentials the project needs: owner-only, excluded from git, never replaced without a yes. | none | `write` |
| `ToolChecker` | none (system concept `ToolCheck`) | Detects the required and optional tools. | none | `check` |
| `Preflight` | none (system concept `PreflightResult`) | Runs the read-only checks of both hosts before anything is created. | none | `check` |
| `GitHost` | Git Host | The operations every host offers: check the token, check that an owner accepts new repositories, check that a name is free. | `name`, `webAddress`, `apiAddress` | `verifyToken`, `ownerAccepts`, `nameFree` |
| `GiteaClient` | Git Host (Gitea) | Hides the Gitea API and its token; creates the repository and the push mirror. | none beyond `GitHost` (uses `Configuration`) | `GiteaClient`, `hasLicense`, `createRepository`, `addPushMirror`, `requestSync` |
| `GitHubClient` | Git Host (GitHub) | Hides the GitHub API and its token; creates the empty repository. | none beyond `GitHost` (uses `Configuration`) | `GitHubClient`, `createEmptyRepository` |
| `LocalProjectBuilder` | Local Project, Remote | Creates the project directory, its git repository and its credential-free remotes. | none | `build` |
| `FrameworkInstaller` | Framework, Framework Setup, Template | Adds the framework submodule, installs skills and hooks once, and copies the templates without overwriting. | none | `install` |
| `SummaryReport` | Summary | Composes the report of what was created, skipped or failed. | none | `compose` |
| `Run` | none (system concept) | Holds the state of one execution. | `isApply` | none |
| `Configuration` | Configuration | Holds the service addresses, the credentials and any preset project details. | `giteaUrl`, `giteaApiUrl`, `githubWebUrl`, `githubApiUrl`, `giteaSshPort`, `mirrorInterval`, `frameworkRepo`, `presetDetails` | none |
| `Credential` | Access Token | Holds a secret in memory only; it never becomes part of an address or a message. | `kind`, `value` | none |
| `ToolCheck` | none (system concept) | Records which tools are present. | `hasGit`, `hasCurl`, `hasJq` | none |
| `PromptSet` | none (system concept) | The questions still to ask; a detail preset in `config.env` is not in it. | `prompts` | none |
| `ProjectRequest` | Project | Holds the details of the project being created. | `name`, `description`, `visibility`, `directory`, `enablePlanGate` | none |
| `Owner` | Owner | A user or organization on a host. | `name`, `kind` | none |
| `PreflightResult` | none (system concept) | Records the outcome of the preflight checks. | `tokensWork`, `ownersAccept`, `nameIsFree`, `licenseIsOffered`, `sshPassed` | none |
| `Repository` | Repository | Common data of a repository on a host. | `name`, `description`, `visibility`, `address` | none |
| `GiteaRepository` | Gitea Repository | The source of truth. | none beyond `Repository` | none |
| `GitHubRepository` | GitHub Repository | Receives its content from the mirror. | none beyond `Repository` | none |
| `LicenseFile` | License | The `AGPL-3.0` file in the Gitea repository when GitHub is chosen. | `key` | none |
| `PushMirror` | Mirror | The Gitea to GitHub push mirror. | `interval`, `syncOnCommit` | none |
| `LocalProject` | Local Project | The project directory on the Maintainer's machine. | `directory` | none |
| `Remote` | Remote | A named link to a repository (`origin`, `github`), without a credential. | `name`, `address` | none |
| `Submodule` | Framework | The framework added to the local project. | `name`, `address` | none |
| `HookSetup` | Framework Setup | Records the skills and hooks installed and the plan gate state. | `areSkillsInstalled`, `areHooksInstalled`, `isPlanGateEnabled` | none |
| `EnvFile` | Credentials File | The `.env` of the project: a copy of the credentials it needs. | `address`, `keys` | none |
| `Template` | Template | A framework file copied into the project. | `name`, `isCopied` | none |
| `InstallResult` | none (carries the result of one operation) | Returns the submodule, the hook setup and the templates of `install`. | none | none |
| `Summary` | Summary | The report returned to the Maintainer; it contains no credential. | `createdItems`, `skippedItems`, `nextSteps` | none |
| `Visibility` | none (enumeration of a Project and Repository attribute) | The two allowed visibilities. | `private`, `public` | none |
## Method Traceability
| Method signature | Operation Contract / SD message |
| --- | --- |
| `ProjectCreator.startProjectCreation() : PromptSet` | [OC-001] `startProjectCreation`; [SD-001] `startProjectCreation()` |
| `ProjectCreator.provideProjectDetails(name, description, visibility, giteaOwner, githubOwner, directory, enablePlanGate, writeEnvFile) : Summary` | [OC-001] `provideProjectDetails`; [SD-001] `provideProjectDetails(...)` |
| `ConfigLoader.load(configFile, envFile) : Configuration` | [SD-001] `load(config.env, .env)`; [OC-001] `startProjectCreation` P2 |
| `CredentialCollector.collect(configuration, kinds) : Configuration` | [SD-001] `collect(configuration, GITEA_TOKEN)` and `collect(configuration, GITHUB_PAT, GITHUB_USER)`; [OC-001] `startProjectCreation` P2 and the precondition of `provideProjectDetails` |
| `EnvFileWriter.write(project, configuration, hasGithub) : EnvFile` | [SD-001] `write(localProject, configuration, githubOwner present)`; [OC-001] `provideProjectDetails` P14 |
| `ToolChecker.check(tools) : ToolCheck` | [SD-001] `check(git, curl, jq)`; [OC-001] `startProjectCreation` P3 |
| `Preflight.check(request) : PreflightResult` | [SD-001] `check(request)`; [OC-001] `provideProjectDetails` P2 |
| `GiteaClient(configuration)` | [SD-001] `new(configuration)` to `GiteaClient` |
| `GitHost.verifyToken() : Boolean` | [SD-001] `verifyToken()` from `Preflight` to either client; P2 |
| `GitHost.ownerAccepts(owner) : Boolean` | [SD-001] `ownerAccepts(giteaOwner)` and `ownerAccepts(githubOwner)`; P2 |
| `GitHost.nameFree(name) : Boolean` | [SD-001] `nameFree(name)` to either client; P2 |
| `GiteaClient.hasLicense(key) : Boolean` | [SD-001] `hasLicense(AGPL-3.0)`; P2 |
| `GiteaClient.createRepository(request, license) : GiteaRepository` | [SD-001] `createRepository(request, license)`; P3, P4 |
| `GiteaClient.addPushMirror(source, target) : PushMirror` | [SD-001] `addPushMirror(giteaRepository, gitHubRepository)`; P6 |
| `GiteaClient.requestSync(mirror) : void` | [SD-001] `requestSync(pushMirror)`; P6 |
| `GitHubClient(configuration)` | [SD-001] `new(configuration)` to `GitHubClient` |
| `GitHubClient.createEmptyRepository(request) : GitHubRepository` | [SD-001] `createEmptyRepository(request)`; P5 |
| `LocalProjectBuilder.build(directory, source, target, sshPassed) : LocalProject` | [SD-001] `build(directory, giteaRepository, gitHubRepository, sshPassed)`; P7, P8, P9 |
| `FrameworkInstaller.install(project, enablePlanGate) : InstallResult` | [SD-001] `install(localProject, enablePlanGate)`; P10, P11, P12 |
| `SummaryReport.compose(request) : Summary` | [SD-001] `compose(projectRequest)`; P13 |
## Pattern Annotations
| Pattern | Classes | Rationale |
| --- | --- | --- |
| Controller (GRASP) | `ProjectCreator` | One entry for the system operations; coordinates and does no HTTP, git or file work itself |
| Facade (GoF) | `GitHost`, `GiteaClient`, `GitHubClient` | Each client hides one host's HTTP API and keeps the token inside; no other class sees a credential. `GitHost` holds the operations both share |
| Pure Fabrication (GRASP) | `ConfigLoader`, `ToolChecker`, `CredentialCollector`, `EnvFileWriter`, `Preflight`, `LocalProjectBuilder`, `FrameworkInstaller`, `SummaryReport` | No domain concept owns these responsibilities; small units keep cohesion high |
| Creator (GRASP) | `ConfigLoader` creates `Configuration`; `GiteaClient` creates `GiteaRepository` and `PushMirror` | The creating class holds the data needed to build the object |
| Protection from variations (GRASP) | `GiteaClient`, `GitHubClient`, `ProjectRequest` | The optional GitHub path is decided by the controller; the clients do not know it |
| Data Transfer Object (GoF-style) | `InstallResult` | Carries the three results of `install` in one return value |
## Dependency Check
No circular dependency. `ProjectCreator` depends on every helper class and no helper depends on it. `Preflight` depends on the two clients; the clients extend `GitHost` and depend only on `Configuration`. The data classes form a tree: `Run` holds `Configuration`, `ToolCheck` and `ProjectRequest`; `ProjectRequest` reaches the repositories and the `LocalProject`; `Summary` points at `ProjectRequest` and nothing points back at it. `CredentialCollector` and `EnvFileWriter` depend only on `Configuration`, `Credential` and `LocalProject`; the only class that holds a secret after the run is `EnvFile`, and only as a copy written to the Maintainer's own disk. `Repository` is shared by `Remote` and `PushMirror` without a cycle.
SOLID check: no class has more than one reason to change (one host API, one kind of local work, one report); the clients can be replaced behind the same operations; the controller depends on the operations, not on how a host or git is called. `ProjectCreator` has two operations and no data, so it is not a god class.
## Implementation Mapping
| Design class | Where it lives in `src/` |
| --- | --- |
| `ProjectCreator` | `create-project.sh` (`main`), `lib/apply.sh` |
| `ConfigLoader` | `lib/config.sh` (`load_configuration`), `lib/validate.sh` |
| `ToolChecker` | `lib/tools.sh` |
| `CredentialCollector` | planned for [MIL-005]: `lib/credentials.sh`, with `lib/prompts.sh` |
| `EnvFileWriter` | planned for [MIL-005]: `lib/envfile.sh` |
| `Preflight` | `lib/preflight.sh` |
| `GitHost`, `GiteaClient`, `GitHubClient` | `lib/api.sh`, `lib/http.sh`, `lib/json.sh`, `lib/repositories.sh`, `lib/mirror.sh`, `lib/hosts.sh` |
| `LocalProjectBuilder` | `lib/localproject.sh`, `lib/git.sh` |
| `FrameworkInstaller` | `lib/framework.sh` |
| `SummaryReport` | `lib/steps.sh`, `lib/plan.sh` |
| `PromptSet`, `ProjectRequest` | `lib/project.sh`, `lib/prompts.sh` |
| `Run`, `Configuration`, `Credential`, `PreflightResult` | the state arrays declared in `lib/constants.sh` |
---
[DCD-001]: ./uc-001/dcd.md
[DM-002]: ./domain-model.md
[UC-001]: ./uc-001/uc.md
[OC-001]: ./uc-001/oc.md
[SD-001]: ./uc-001/sd.md
[MIL-005]: ./milestones/mil-005-credentials.md
[DICT-001]: ./dictionary.md
[f4d611b]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/f4d611b77cc70b4686506d44bf8f439045d9e0d2
[ded26a6]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/ded26a658c666bf29d84093cb352e3635e07719b
+27 -19
View File
@@ -9,7 +9,8 @@
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Initial version, terms of UC-001 | [02875ae] |
| 2026-10-06 | Deprecated | Jens Tirsvad Nielsen | S02 | The IT terms are also used in DCD-001 and DCD-002<br>InstallResult and Visibility named as design-only types | [f4d611b] |
| 2026-10-06 | Accepted | Jens Tirsvad Nielsen | S02 | Added Credentials File / EnvFile | [ded26a6] |
---
@@ -21,23 +22,24 @@ Maps each Product Owner (PO) term to its professional IT term. PO language: Engl
| PO term | Language | IT term | Definition | Used as PO term in | Used as IT term in |
| --- | --- | --- | --- | --- | --- |
| Maintainer | en | Maintainer | The person who creates a new project. | DM, UC, US | OC, SD |
| Project | en | ProjectRequest | The new software project being set up, with its name, description and visibility. | DM, UC, US | OC, SD |
| Configuration | en | Configuration | The service addresses and access tokens set up before starting. | DM, UC | OC, SD |
| Git Host | en | GitHost | A service that holds repositories: Gitea or GitHub. | DM, UC | OC, SD |
| Access Token | en | Credential | A secret that lets the Maintainer act on a Git Host; never part of an address. | DM, UC | OC, SD |
| Owner | en | Owner | The user or organization on a Git Host that owns repositories. | DM, UC | OC, SD |
| Repository | en | Repository | A place on a Git Host that holds a project's history. | DM, UC | OC, SD |
| Gitea Repository | en | GiteaRepository | The repository on Gitea; the source of truth. | DM, UC | OC, SD |
| GitHub Repository | en | GitHubRepository | The repository on GitHub; it receives its content from the mirror. | DM, UC | OC, SD |
| License | en | LicenseFile | The legal terms file (AGPL-3.0) added to the Gitea repository when GitHub is chosen. | DM, UC | OC, SD |
| Mirror | en | PushMirror | The push mirror that copies a Gitea repository to a GitHub repository. | DM, UC | OC, SD |
| Local Project | en | LocalProject | The project directory on the Maintainer's machine. | DM, UC | OC, SD |
| Remote | en | Remote | A named link from a local project to a repository. | DM, UC | OC, SD |
| Framework | en | Submodule | The SQA-QC-Framework added to a local project; the IT term names how it is attached. | DM, UC | OC, SD |
| Framework Setup | en | HookSetup | The skills and git hooks installed from the framework, with the plan gate on or off. | DM, UC | OC, SD |
| Template | en | Template | A framework file copied into a project. | DM, UC | OC, SD |
| Summary | en | Summary | The report of what was created, skipped or failed and how to continue. | DM, UC | OC, SD |
| Maintainer | en | Maintainer | The person who creates a new project. | DM, UC, US | OC, SD, DCD |
| Project | en | ProjectRequest | The new software project being set up, with its name, description and visibility. | DM, UC, US | OC, SD, DCD |
| Configuration | en | Configuration | The service addresses and access tokens set up before starting. | DM, UC | OC, SD, DCD |
| Git Host | en | GitHost | A service that holds repositories: Gitea or GitHub. | DM, UC | OC, SD, DCD |
| Access Token | en | Credential | A secret that lets the Maintainer act on a Git Host; never part of an address. | DM, UC | OC, SD, DCD |
| Owner | en | Owner | The user or organization on a Git Host that owns repositories. | DM, UC | OC, SD, DCD |
| Repository | en | Repository | A place on a Git Host that holds a project's history. | DM, UC | OC, SD, DCD |
| Gitea Repository | en | GiteaRepository | The repository on Gitea; the source of truth. | DM, UC | OC, SD, DCD |
| GitHub Repository | en | GitHubRepository | The repository on GitHub; it receives its content from the mirror. | DM, UC | OC, SD, DCD |
| License | en | LicenseFile | The legal terms file (AGPL-3.0) added to the Gitea repository when GitHub is chosen. | DM, UC | OC, SD, DCD |
| Mirror | en | PushMirror | The push mirror that copies a Gitea repository to a GitHub repository. | DM, UC | OC, SD, DCD |
| Local Project | en | LocalProject | The project directory on the Maintainer's machine. | DM, UC | OC, SD, DCD |
| Remote | en | Remote | A named link from a local project to a repository. | DM, UC | OC, SD, DCD |
| Framework | en | Submodule | The SQA-QC-Framework added to a local project; the IT term names how it is attached. | DM, UC | OC, SD, DCD |
| Framework Setup | en | HookSetup | The skills and git hooks installed from the framework, with the plan gate on or off. | DM, UC | OC, SD, DCD |
| Template | en | Template | A framework file copied into a project. | DM, UC | OC, SD, DCD |
| Credentials File | en | EnvFile | The file in the local project that holds a copy of the credentials the project needs; owner-only and ignored by git. | DM, UC | OC, SD, DCD |
| Summary | en | Summary | The report of what was created, skipped or failed and how to continue. | DM, UC | OC, SD, DCD |
## Rules
@@ -47,6 +49,10 @@ Maps each Product Owner (PO) term to its professional IT term. PO language: Engl
- `Run`, `ToolCheck`, `PreflightResult` and `PromptSet` appear in [OC-001] but
have no PO term: they are system concepts, not domain concepts, and are not
in the Domain Model.
- `InstallResult` and the enumeration `Visibility` appear only in [DCD-001]:
`InstallResult` carries the three results of one operation, and `Visibility`
is the type of the Project and Repository attribute of the same name. Neither
is a domain concept.
- A new concept in a Domain Model gets a row here in the same change.
---
@@ -56,4 +62,6 @@ Maps each Product Owner (PO) term to its professional IT term. PO language: Engl
[DM-001]: ./uc-001/dm.md
[DM-002]: ./domain-model.md
[OC-001]: ./uc-001/oc.md
[02875ae]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/02875aee5f2953473924074eea0056eb31af6b7a
[DCD-001]: ./uc-001/dcd.md
[f4d611b]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/f4d611b77cc70b4686506d44bf8f439045d9e0d2
[ded26a6]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/ded26a658c666bf29d84093cb352e3635e07719b
+11 -3
View File
@@ -9,8 +9,8 @@
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Initial version, created from [DM-001] (UC-001) | [02875ae] |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Configuration may hold preset project details (from DM-001, UC-001 step 3) | [2a6bb8e] |
| 2026-10-05 | Deprecated | Jens Tirsvad Nielsen | S02 | Configuration may hold preset project details (from DM-001, UC-001 step 3) | [2a6bb8e] |
| 2026-10-06 | Accepted | Jens Tirsvad Nielsen | S02 | Added Credentials File (from DM-001, UC-001 step 9) | [ded26a6] |
---
@@ -79,6 +79,9 @@ class "Framework Setup" as FrameworkSetup {
class Template {
name
}
class "Credentials File" as CredentialsFile {
address
}
class Summary {
created items
skipped items
@@ -108,6 +111,8 @@ LocalProject "1" --> "1" FrameworkSetup : has
FrameworkSetup "0..*" --> "1" Framework : is installed from
Framework "1" --> "1..*" Template : provides
LocalProject "1" --> "0..*" Template : contains a copy of
LocalProject "1" --> "0..1" CredentialsFile : has
CredentialsFile "1" --> "1..2" AccessToken : holds a copy of
Summary "1" --> "1" Project : reports on
@enduml
```
@@ -132,6 +137,7 @@ Summary "1" --> "1" Project : reports on
| Framework | The SQA-QC-Framework added to a Local Project | name, address | [UC-001] step 9 "framework submodule" |
| Framework Setup | The skills and git hooks installed from the Framework, with the plan gate on or off | plan gate enabled | [UC-001] step 9 "skills and hooks", "plan gate" |
| Template | A file the Framework provides to copy into a project (`AGENTS.md`, artifact registry) | name | [UC-001] step 9 "templates" |
| Credentials File | The file in a Local Project that holds a copy of the Access Tokens (and the GitHub account name) the project needs; readable by its owner only and ignored by git | address | [UC-001] step 9 "credentials file" |
| Summary | The report of what was created, skipped or failed and how to continue | created items, skipped items, next steps | [UC-001] step 10 "summary" |
## Association Table
@@ -158,6 +164,8 @@ Summary "1" --> "1" Project : reports on
| Framework Setup | is installed from | Framework | 0..* to 1 |
| Framework | provides | Template | 1 to 1..* |
| Local Project | contains a copy of | Template | 1 to 0..* |
| Local Project | has | Credentials File | 1 to 0..1 |
| Credentials File | holds a copy of | Access Token | 1 to 1..2 |
| Summary | reports on | Project | 1 to 1 |
## Generalizations
@@ -172,5 +180,5 @@ Summary "1" --> "1" Project : reports on
[SSD-001]: ./uc-001/ssd.md
[DICT-001]: ./dictionary.md
[DM-001]: ./uc-001/dm.md
[02875ae]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/02875aee5f2953473924074eea0056eb31af6b7a
[2a6bb8e]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/2a6bb8e8afadfe6ca4a621da30e44a372898ca62
[ded26a6]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/ded26a658c666bf29d84093cb352e3635e07719b
+82
View File
@@ -0,0 +1,82 @@
# MIL-005 Credentials
## Metadata
| Key | Value |
| --- | --- |
| ID | MIL-005 |
| CrossReference | [BC-001], [US-001], [UC-001], [DCD-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-06 | Accepted | Jens Tirsvad Nielsen | S02 | Initial version | [ded26a6] |
---
## Purpose
Decide whether the script can ask for a credential that `.env` does not provide and create the new project's own `.env`, without exposing a token: asked without echo, written only after a yes, owner-only, ignored by git, never replacing an existing file, and never shown in any output.
## Deliverable
`create-project.sh` that (1) no longer stops when `.env` is missing or lacks a credential but asks for it without echo (`GITEA_TOKEN` always; `GITHUB_PAT` and `GITHUB_USER` once GitHub is chosen), validating each value like one read from `.env`; and (2) after the local project exists, asks whether to create a `.env` in it and, on a yes, writes only the keys the project needs: `GITEA_TOKEN`, and `GITHUB_PAT` and `GITHUB_USER` when GitHub was chosen. The file is readable by its owner only, excluded from git through `.git/info/exclude` (no tracked file changes, nothing is committed) and never replaced without a yes. The README and `.env.example` document it. The tests cover every case.
This changes a security guarantee of the earlier milestones ("a token is never persisted"): a token may now be written to one file, and only after a yes. [BC-001] objective 6, success criterion 1 and the risk table are amended in the same change.
## Go / No-Go Criteria
| # | Criterion (objectively checkable) | Go | No-Go |
| --- | --- | --- | --- |
| 1 | `GITEA_TOKEN` missing from `.env`, or `.env` absent: it is asked, not echoed, validated like a token read from `.env`, and the run continues; the same for `GITHUB_PAT` and `GITHUB_USER` once GitHub is chosen, and never for them when GitHub is not chosen | Tests pass | The run stops with an error, a value is echoed or GitHub credentials are asked without GitHub |
| 2 | An invalid asked value is refused and asked again without showing it; when input ends the run stops before any request to a host and names the key | Tests pass | A request made or a value shown |
| 3 | A credential provided in `.env` is not asked | Tests pass | Any prompt shown |
| 4 | The "create `.env`" question is asked after the local project exists and defaults to no; on no, no file is created and the summary says so | Tests pass | A file written without a yes |
| 5 | On yes the new project's `.env` exists, holds exactly the needed keys (`GITEA_TOKEN`; plus `GITHUB_PAT` and `GITHUB_USER` when GitHub was chosen), has an owner-only mode (600) from the moment it is created, and `git status` in the project does not list it | Tests pass | Another key, another mode or the file listed |
| 6 | An existing `.env` in the project is never replaced without a yes; on no it is kept unchanged and reported | Tests pass | Any replaced without a yes |
| 7 | No token appears in any output, summary, log, remote URL, tracked file or file other than the project's `.env`; searched in every file of the new project and in all output of the tests, including under `bash -x` | Tests pass | Any hit |
| 8 | Only the project's `.env` changed on disk by this feature: no tracked file, no `.gitignore`, no global git configuration is written | Tests pass | Any other change |
| 9 | All acceptance criteria of US-001.05 in [US-001] are met | Verified | Any unmet |
## Dependencies
| Depends on | Reason |
| --- | --- |
| [MIL-004] | Needs the prompt flow and the configuration presets it changed |
## Traceability
| Business Case objective / KPI / user story | Reference |
| --- | --- |
| User story US-001.05 | [US-001] |
| Objective 9 (credentials asked, project `.env`) and the amended objective 6 | [BC-001] |
| Success criteria 1 and 9 | [BC-001] |
## Ownership
| Role | Stakeholder ID (SA) |
| --- | --- |
| Owner | S01 |
| Approving reviewer | S02 |
## Target Date
2026-11-27 — proposed; the Business Case sets no deadline.
## Tasks
| # | Task | Summary | Needs its own Use Case/User Story? | Reference |
| --- | --- | --- | --- | --- |
| 1 | Ask for a credential that `.env` does not provide | `.env` becomes optional. `GITEA_TOKEN` is asked after the configuration is read; `GITHUB_PAT` and `GITHUB_USER` once GitHub is chosen. Read without echo (`read -s`), validated by the existing token and account validators, registered for redaction before any later message, asked again when invalid, a stop naming the key when input ends. A new `lib/credentials.sh` (class `CredentialCollector` of [DCD-001]). Extension 2b of [UC-001]. | Yes | [UC-001] |
| 2 | Create the new project's `.env` | After the local project exists and only after a yes (default no): write the needed keys to `.env` created with `umask 077` and mode 600, never replacing an existing file without a yes, and add `.env` to `.git/info/exclude`. Report it in the summary without showing a value. A new `lib/envfile.sh` (class `EnvFileWriter`). Step 9 and extensions 9c and 9d of [UC-001]. | Yes | [UC-001] |
| 3 | Keep every token out of everything else | The `bash -x` guard, the redaction and the temporary files keep working with credentials that are asked; the asked value never reaches a command line, output, summary or a file other than the project's `.env`. Add the new function groups to the library layout. | No | |
| 4 | Document the feature and its risk | README: credentials may be asked, the project `.env`, what it holds, why it is owner-only and ignored, the risk of a plain-text token on disk, how to say no. `.env.example` and the security decisions section updated. | No | |
| 5 | Test every case | Each credential present, missing and invalid; `.env` absent; no GitHub credentials without GitHub; end of input; the question defaults to no; file contents, mode and git exclusion; an existing `.env`; every token searched for in the output and the project; the existing tests unchanged. | No | |
---
[BC-001]: ../business-case.md
[US-001]: ../user-stories.md
[UC-001]: ../uc-001/uc.md
[DCD-001]: ../uc-001/dcd.md
[MIL-004]: ./mil-004-configurable-details.md
[ded26a6]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/ded26a658c666bf29d84093cb352e3635e07719b
+12 -7
View File
@@ -4,23 +4,23 @@
| Key | Value |
| --- | --- |
| ID | PP-001 |
| CrossReference | [BC-001], [SA-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [US-001] |
| CrossReference | [BC-001], [SA-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [US-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Stories per phase: US-001.01 to US-001.03<br>Dates accepted | [02875ae] |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Added phase MIL-004 (proposed dates 2026-11-16 to 2026-11-20) | [2a6bb8e] |
| 2026-10-05 | Deprecated | Jens Tirsvad Nielsen | S02 | Added phase MIL-004 (proposed dates 2026-11-16 to 2026-11-20) | [2a6bb8e] |
| 2026-10-06 | Accepted | Jens Tirsvad Nielsen | S02 | Added phase MIL-005 (proposed dates 2026-11-23 to 2026-11-27) | [ded26a6] |
---
## Purpose
Schedule the four phases that deliver RepoFoundry (`create-project.sh` and its documentation) in dependency order. The Business Case sets no deadline, so the dates below are proposals for S01 to confirm.
Schedule the five phases that deliver RepoFoundry (`create-project.sh` and its documentation) in dependency order. The Business Case sets no deadline, so the dates below are proposals for S01 to confirm.
## Planning Assumptions
- Week 1 starts 2026-10-05; the plan ends by 2026-11-20 (the last phase is proposed).
- Week 1 starts 2026-10-05; the plan ends by 2026-11-27 (the last phase is proposed).
- Phase length: two weeks.
- S01 and S02 review each phase through a pull request, as described in [SA-001]. For now one person holds both roles.
- The PO language is English, so no translated copies are kept.
@@ -33,6 +33,7 @@ Schedule the four phases that deliver RepoFoundry (`create-project.sh` and its d
| Repositories and Mirror | [MIL-002] | 2026-10-19 to 2026-10-30 | 2026-10-30 | S02 | US-001.02 | GitHub and Gitea repositories and the push mirror | [Milestone 44] |
| Scaffold and Release | [MIL-003] | 2026-11-02 to 2026-11-13 | 2026-11-13 | S01 | US-001.03 | Local project, framework, README, final review | [Milestone 45] |
| Configurable Details | [MIL-004] | 2026-11-16 to 2026-11-20 | 2026-11-20 | S01 | US-001.04 | Project details preset in config.env | |
| Credentials | [MIL-005] | 2026-11-23 to 2026-11-27 | 2026-11-27 | S01 | US-001.05 | Missing credentials asked; project .env | |
```plantuml
@startgantt
@@ -45,6 +46,8 @@ Project starts 2026-10-05
[Scaffold and Release Go/No-Go] happens 2026-11-13
[Configurable Details] starts 2026-11-16 and ends 2026-11-20
[Configurable Details Go/No-Go] happens 2026-11-20
[Credentials] starts 2026-11-23 and ends 2026-11-27
[Credentials Go/No-Go] happens 2026-11-27
@endgantt
```
@@ -59,11 +62,12 @@ Project starts 2026-10-05
| Local directory, remotes, framework submodule, skills, hooks, templates, plan gate | [MIL-003] |
| README and SSH prerequisite documentation | [MIL-003] |
| Project details set in `config.env` instead of asked | [MIL-004] |
| Missing credentials asked; the new project's `.env` | [MIL-005] |
## Dependencies
```
MIL-001 → MIL-002 → MIL-003 → MIL-004
MIL-001 → MIL-002 → MIL-003 → MIL-004 → MIL-005
```
A No-Go moves every later date by the time needed to rework the failed criteria.
@@ -91,11 +95,12 @@ A No-Go moves every later date by the time needed to rework the failed criteria.
[MIL-002]: ./milestones/mil-002-repositories-and-mirror.md
[MIL-003]: ./milestones/mil-003-scaffold-and-release.md
[MIL-004]: ./milestones/mil-004-configurable-details.md
[MIL-005]: ./milestones/mil-005-credentials.md
[US-001]: ./user-stories.md
[UC-001]: ./uc-001/uc.md
[SSD-001]: ./uc-001/ssd.md
[02875ae]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/02875aee5f2953473924074eea0056eb31af6b7a
[Milestone 43]: https://git.tirsystem.com/TirSystem-BashScript/RepoFoundry/milestone/43
[Milestone 44]: https://git.tirsystem.com/TirSystem-BashScript/RepoFoundry/milestone/44
[Milestone 45]: https://git.tirsystem.com/TirSystem-BashScript/RepoFoundry/milestone/45
[2a6bb8e]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/2a6bb8e8afadfe6ca4a621da30e44a372898ca62
[ded26a6]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/ded26a658c666bf29d84093cb352e3635e07719b
+74
View File
@@ -0,0 +1,74 @@
# SQA Review Record: MIL-005 and the planning change it causes
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-020 |
| CrossReference | [MIL-005], [QC-MIL-001], [US-001], [UC-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-06 | Proposed | Jens Tirsvad Nielsen | S02 | Initial version | [ded26a6] |
---
## Artifact Under Review
- Instance reviewed: [MIL-005], and the changes it causes in [BC-001], [US-001], [UC-001], [SSD-001], [OC-001], [SD-001], [DM-001], [DM-002], [DICT-001] and [PP-001]. The two Design Class Diagrams that change with it are reviewed in [RC-021].
- Checklist used: [QC-MIL-001] for [MIL-005]. The other artifacts were changed, not created; their change is checked below for consistency with the checklists of their types and with the PP reference.
- Review date: 2026-10-06
## Checklist Results ([MIL-005], QC-MIL-001)
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | A concrete deliverable is defined for every gate | Pass | A script that asks for a missing credential and creates the new project's `.env`, the documentation of the feature and its risk, and tests for every case. |
| 2 | Explicit Go/No-Go criteria are stated for each gate | Pass | Nine criteria, each with an objective Go and No-Go, including all acceptance criteria of US-001.05. |
| 3 | Dependencies on other milestones are explicitly mapped | Pass | Depends on [MIL-004], with the reason. |
| 4 | Each milestone is traceable to a Business Case objective or KPI | Pass | Maps to objective 9, the amended objective 6 and success criteria 1 and 9 of [BC-001], and to US-001.05. |
| 5 | Milestone owner and approving reviewer are identified | Pass | Owner S01, approving reviewer S02. |
| 6 | Milestone has a defined target date consistent with project constraints | Pass | 2026-11-27 matches [PP-001]; the Business Case sets no duration, so nothing conflicts. The date is a proposal and is accepted here. |
## Change checks on the other artifacts
| Artifact | Change | Status | Evidence/Notes |
| --- | --- | --- | --- |
| [BC-001] | Objective 9, scope items, success criterion 9, a risk and a constraint; objective 6 and success criterion 1 amended | Pass | The security guarantee is weakened on purpose and said so in the same place: a token may be written only to the new project's `.env`, after a yes. The risk row lists the mitigations. |
| [US-001] | US-001.05 with five acceptance criteria | Pass | Given/when/then; traces to [UC-001] and [MIL-005]; the last criterion keeps the "no credential in output" rule. |
| [UC-001] | Precondition, step 2 and 9 notes, extensions 2b, 9c, 9d, a postcondition and two business rules | Pass | Extension 2b ends before any change when input ends; 9c and 9d keep "never replaced without a yes". |
| [SSD-001] | `writeEnvFile` and the credentials not provided in `.env` become parameters; the lifecycle note names the one thing that persists | Pass | One new parameter and a note; the operations are unchanged. |
| [OC-001] | Postcondition P14, a precondition, two exceptions, and P2 reworded | Pass | P14 states the contents, the mode, the git exclusion and "no credential shown". |
| [SD-001] | `CredentialCollector` and `EnvFileWriter` and their messages | Pass | Every new postcondition has a message; the coverage table is updated. Three lines damaged by an earlier edit (`actor Maintainer`, the first `provideProjectDetails` message, the final `summary` return) are restored in this change. |
| [DM-001], [DM-002] | Concept `Credentials File` and two associations | Pass | Both models changed in the same way. |
| [DICT-001] | `Credentials File` ↔ `EnvFile` | Pass | One PO term and one IT term, as the dictionary rules require. |
| [PP-001] | Phase [MIL-005], dependency chain, timeline | Pass | Dates agree with [MIL-005]. |
One point for the implementation: the Go/No-Go criterion 5 says the file has mode 600 "from the moment it is created". That means the script must create it under `umask 077` (or with `install -m 600`) and not write it first and restrict it afterwards. Task 2 already says so.
## Overall Verdict
Go — [MIL-005] passes every mandatory criterion and the changes to the other artifacts are consistent with each other. The weakening of the credential guarantee is deliberate, bounded and recorded in the Business Case. Author and reviewer are the same person for now (S01 and S02 are both held by the Maintainer), so the framework independence rule is not met; re-review when a second person takes S02.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| None | - | - |
---
[MIL-005]: ../../milestones/mil-005-credentials.md
[BC-001]: ../../business-case.md
[US-001]: ../../user-stories.md
[UC-001]: ../../uc-001/uc.md
[SSD-001]: ../../uc-001/ssd.md
[OC-001]: ../../uc-001/oc.md
[SD-001]: ../../uc-001/sd.md
[DM-001]: ../../uc-001/dm.md
[DM-002]: ../../domain-model.md
[DICT-001]: ../../dictionary.md
[PP-001]: ../../project-plan.md
[RC-021]: ./rc-021-dcd.md
[QC-MIL-001]: ../../../framework/qc/qc-milestones-gateways.md
[ded26a6]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/ded26a658c666bf29d84093cb352e3635e07719b
+64
View File
@@ -0,0 +1,64 @@
# SQA Review Record: Design Class Diagrams DCD-001 and DCD-002
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-021 |
| CrossReference | [DCD-001], [DCD-002], [QC-DCD-001], [SD-001], [OC-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-06 | Proposed | Jens Tirsvad Nielsen | S02 | Initial version | [ded26a6] |
---
## Artifact Under Review
- Instances reviewed: [DCD-001] (use case UC-001) and [DCD-002] (the consolidated project model). [DCD-002] was created from [DCD-001] and the two have the same classes, relationships and tables.
- Checklist used: [QC-DCD-001]
- Review date: 2026-10-06
- PlantUML: the diagrams were not rendered. `render-diagrams.sh` needs a PlantUML server and sends the diagram text to it; none is configured. The syntax was read by hand (see finding F3).
## Checklist Results
| # | Criterion | Level | Status | Evidence/Notes |
| --- | --- | --- | --- | --- |
| 1 | SOLID principles applied; no god classes | Mandatory | Pass | Each class has one reason to change: one host API each (`GiteaClient`, `GitHubClient`), local work (`LocalProjectBuilder`), the framework (`FrameworkInstaller`), prompts for credentials (`CredentialCollector`), the project `.env` (`EnvFileWriter`), the report (`SummaryReport`). `ProjectCreator` has two operations and no data. The clients share `GitHost` instead of repeating its three operations. |
| 2 | Visibility markers correct and consistent | Mandatory | Pass | Every attribute and operation has `+` or `-`; the only private operation is `GiteaClient.requestSync`, which no other class calls. Enumeration literals carry no marker, as is usual. |
| 3 | Association, aggregation, composition and dependency correctly distinguished | Mandatory | Pass | Composition where the part cannot outlive the whole (`Run`, `Configuration`, `LocalProject` and their parts); plain association for the links between independent objects; dependency for "creates" and "asks"; generalization for `Repository` and `GitHost`. No aggregation is used. |
| 4 | Multiplicities and navigability specified on all associations | Mandatory | Pass after fix | Found during this review: most associations gave only the target multiplicity. Fixed: both ends now carry a multiplicity and every association has one arrow. Dependencies carry none, as UML does not give them one. |
| 5 | Applied design patterns annotated | Optional | Pass | The Pattern Annotations table names Controller, Facade, Pure Fabrication, Creator, Protection from variations and a data transfer object. |
| 6 | Method signatures traceable to Operation Contracts and Sequence Diagrams | Mandatory | Pass | The Method Traceability table has a row for each of the 20 operations, each naming the [SD-001] message and the contract postcondition. [SD-001] was aligned in the same change (`createRepository(request, license)`, `compose(request)` and others). |
| 7 | Class names consistent with the Domain Model concepts they refine | Mandatory | Pass | The IT terms of [DICT-001] are used (`ProjectRequest` for Project, `Credential` for Access Token, `EnvFile` for Credentials File, and so on). `GitHost` is a class of its own, as the dictionary has one IT term for the PO term. The system concepts without a PO term (`Run`, `ToolCheck`, `PreflightResult`, `PromptSet`, `InstallResult`) are marked as such in the class table. |
| 8 | No circular dependencies | Optional | Pass | Stated and argued in the Dependency Check; `Summary` points at `ProjectRequest` and nothing points back; the helper classes depend on the data classes and the controller, never the other way round. |
## Findings
| # | Finding | Severity | Status |
| --- | --- | --- | --- |
| F1 | Associations gave only the target multiplicity (criterion 4). | Defect | Fixed in both files. |
| F2 | The planned classes `CredentialCollector`, `EnvFileWriter` and `EnvFile` (milestone [MIL-005]) are already in the diagram while the code does not exist yet. | Info | Accepted: the Implementation Mapping marks them "planned for MIL-005", so the diagram is a design for code still to come. |
| F3 | The PlantUML text was not rendered by a tool. | Low | Open: render with `render-diagrams.sh` once a server is chosen. Constructs used are standard (`abstract class`, `enum`, stereotypes, multiplicities, `skinparam`, `hide empty members`). |
| F4 | `Credential` is also the kind of `GITHUB_USER`, which is an account name, not a secret. The dictionary maps Access Token to `Credential`. | Info | Accepted: `kind` tells them apart; `Configuration` holds one to three of them. |
## Overall Verdict
Go — all mandatory criteria pass after the fix for criterion 4, and the optional ones pass. F3 is open and not blocking. Author and reviewer are the same person for now (S01 and S02 are both held by the Maintainer), so the framework independence rule is not met; re-review when a second person takes S02.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Render the diagrams of [DCD-001], [DCD-002], [SD-001] and the other PlantUML blocks with `render-diagrams.sh` once the Maintainer chooses a server | S01 | 2026-10-16 |
---
[DCD-001]: ../../uc-001/dcd.md
[DCD-002]: ../../dcd.md
[SD-001]: ../../uc-001/sd.md
[OC-001]: ../../uc-001/oc.md
[DICT-001]: ../../dictionary.md
[MIL-005]: ../../milestones/mil-005-credentials.md
[QC-DCD-001]: ../../../framework/qc/qc-dcd.md
[ded26a6]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/ded26a658c666bf29d84093cb352e3635e07719b
+23 -15
View File
@@ -9,8 +9,8 @@
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-05 | Proposed | Jens Tirsvad Nielsen | S02 | Initial version, UC-001 artifacts and baseline | [02875ae] |
| 2026-10-05 | Proposed | Jens Tirsvad Nielsen | S02 | Added MIL-004 and RC-018 | [2a6bb8e] |
| 2026-10-06 | Proposed | Jens Tirsvad Nielsen | S02 | Added DCD-001 and DCD-002 | [f4d611b] |
| 2026-10-06 | Proposed | Jens Tirsvad Nielsen | S02 | Added MIL-005, RC-020 and RC-021 | [ded26a6] |
---
@@ -24,27 +24,30 @@ updated whenever an artifact instance is created or reviewed.
| Artifact Instance | Type | Upstream (Backward Link) | Downstream (Forward Link) | Last Reviewed (RC-ID) |
| --- | --- | --- | --- | --- |
| [BC-001] | BC | - | [SA-001], [PP-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [US-001], [UCD-001] | [RC-010], [RC-018] |
| [BC-001] | BC | - | [SA-001], [PP-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [US-001], [UCD-001] | [RC-010], [RC-018], [RC-020] |
| [SA-001] | SA | [BC-001] | [UCD-001], [UC-001], [DICT-001] | [RC-013] |
| [PP-001] | PP | [BC-001], [SA-001] | [MIL-001], [MIL-002], [MIL-003], [MIL-004] | [RC-012], [RC-018] |
| [PP-001] | PP | [BC-001], [SA-001] | [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005] | [RC-012], [RC-018], [RC-020] |
| [MIL-001] | MIL | [BC-001], [PP-001] | [US-001] | [RC-011], [RC-016] |
| [MIL-002] | MIL | [BC-001], [PP-001] | [US-001] | [RC-014], [RC-017] |
| [MIL-003] | MIL | [BC-001], [PP-001] | [US-001] | [RC-015], [RC-017] |
| [MIL-004] | MIL | [BC-001], [PP-001] | [US-001] | [RC-018], [RC-019] |
| [MIL-005] | MIL | [BC-001], [PP-001] | [US-001] | [RC-020] |
| [UCD-001] | UCD | [BC-001], [SA-001] | [US-001], [UC-001] | [RC-009] |
| [US-001] | US | [BC-001], [UCD-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004] | [UC-001] | [RC-001] |
| [UC-001] | UC | [UCD-001], [US-001], [SA-001] | [SSD-001], [DM-001] | [RC-002] |
| [SSD-001] | SSD | [UC-001] | [OC-001] | [RC-003] |
| [DM-001] | DM | [UC-001], [SSD-001] | [DM-002], [DICT-001], [OC-001] | [RC-004] |
| [DM-002] | DM | [DM-001] | [DICT-001] | [RC-005] |
| [DICT-001] | DICT | [BC-001], [SA-001], [DM-001], [DM-002] | [OC-001], [SD-001] | [RC-008] |
| [OC-001] | OC | [SSD-001], [DM-001] | [SD-001] | [RC-006] |
| [SD-001] | SD | [OC-001] | - | [RC-007] |
| [US-001] | US | [BC-001], [UCD-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005] | [UC-001] | [RC-001], [RC-020] |
| [UC-001] | UC | [UCD-001], [US-001], [SA-001] | [SSD-001], [DM-001] | [RC-002], [RC-020] |
| [SSD-001] | SSD | [UC-001] | [OC-001] | [RC-003], [RC-020] |
| [DM-001] | DM | [UC-001], [SSD-001] | [DM-002], [DICT-001], [OC-001], [DCD-001] | [RC-004], [RC-020] |
| [DM-002] | DM | [DM-001] | [DICT-001], [DCD-001], [DCD-002] | [RC-005], [RC-020] |
| [DICT-001] | DICT | [BC-001], [SA-001], [DM-001], [DM-002] | [OC-001], [SD-001] | [RC-008], [RC-020] |
| [OC-001] | OC | [SSD-001], [DM-001] | [SD-001] | [RC-006], [RC-020] |
| [SD-001] | SD | [OC-001] | [DCD-001] | [RC-007], [RC-020], [RC-021] |
| [DCD-001] | DCD | [UC-001], [DM-001], [DM-002], [OC-001], [SD-001], [DICT-001] | [DCD-002] | [RC-021] |
| [DCD-002] | DCD | [DCD-001], [DM-002], [DICT-001] | - | [RC-021] |
## Coverage Notes
- Reviewed so far: every artifact in the project (see the Last Reviewed column).
- No Design Class Diagram, ERD, KPI, BMC or BPMN exists yet. `-` in Downstream means nothing is built on the artifact yet.
- No ERD, KPI, BMC or BPMN exists yet. `-` in Downstream means nothing is built on the artifact yet.
---
@@ -55,8 +58,13 @@ updated whenever an artifact instance is created or reviewed.
[MIL-002]: ../milestones/mil-002-repositories-and-mirror.md
[MIL-003]: ../milestones/mil-003-scaffold-and-release.md
[MIL-004]: ../milestones/mil-004-configurable-details.md
[MIL-005]: ../milestones/mil-005-credentials.md
[RC-018]: ./reviews/rc-018-mil-004.md
[RC-019]: ./reviews/rc-019-mil-004-code.md
[RC-020]: ./reviews/rc-020-mil-005.md
[RC-021]: ./reviews/rc-021-dcd.md
[DCD-001]: ../uc-001/dcd.md
[DCD-002]: ../dcd.md
[UCD-001]: ../use-case-diagram.md
[US-001]: ../user-stories.md
[UC-001]: ../uc-001/uc.md
@@ -83,5 +91,5 @@ updated whenever an artifact instance is created or reviewed.
[RC-015]: ./reviews/rc-015-mil-003.md
[RC-016]: ./reviews/rc-016-create-project-sh.md
[RC-017]: ./reviews/rc-017-e2e-security-review.md
[02875ae]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/02875aee5f2953473924074eea0056eb31af6b7a
[2a6bb8e]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/2a6bb8e8afadfe6ca4a621da30e44a372898ca62
[f4d611b]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/f4d611b77cc70b4686506d44bf8f439045d9e0d2
[ded26a6]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/ded26a658c666bf29d84093cb352e3635e07719b
+340
View File
@@ -0,0 +1,340 @@
# Design Class Diagram (UC-001)
## Metadata
| Key | Value |
| --- | --- |
| ID | DCD-001 |
| CrossReference | [UC-001], [DM-001], [DM-002], [OC-001], [SD-001], [DICT-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-06 | Deprecated | Jens Tirsvad Nielsen | S02 | Initial version | [f4d611b] |
| 2026-10-06 | Accepted | Jens Tirsvad Nielsen | S02 | Added CredentialCollector, EnvFileWriter and EnvFile; writeEnvFile parameter | [ded26a6] |
---
## Purpose and Scope
Covers [UC-001] "Create a new project". It refines the concepts of [DM-001] into design classes and turns the messages of [SD-001] into method signatures, so that every method traces to a contract in [OC-001] or to a message in [SD-001]. Class and attribute names are the IT terms of [DICT-001]. The project-level model that consolidates all use cases is [DCD-002].
The classes are design classes of a Bash program: a class is a group of functions in `src/lib/` with its data held in the shared state arrays (see "Implementation Mapping"). There is no object-oriented runtime, but the responsibilities, associations and dependencies below are the ones the code keeps.
Failure handling (the exceptions of [OC-001]) is one rule of `ProjectCreator`, stop and report, and is not drawn.
## Diagram
```plantuml
@startuml
skinparam classAttributeIconSize 0
hide empty members
enum Visibility {
private
public
}
class ProjectCreator <<controller>> {
+startProjectCreation() : PromptSet
+provideProjectDetails(name : String, description : String, visibility : Visibility, giteaOwner : Owner, githubOwner : Owner [0..1], directory : Path, enablePlanGate : Boolean, writeEnvFile : Boolean) : Summary
}
class ConfigLoader {
+load(configFile : Path, envFile : Path) : Configuration
}
class CredentialCollector {
+collect(configuration : Configuration, kinds : String [1..3]) : Configuration
}
class EnvFileWriter {
+write(project : LocalProject, configuration : Configuration, hasGithub : Boolean) : EnvFile [0..1]
}
class ToolChecker {
+check(tools : String [1..*]) : ToolCheck
}
class Preflight {
+check(request : ProjectRequest) : PreflightResult
}
abstract class GitHost <<facade>> {
-name : String
-webAddress : String
-apiAddress : String
+verifyToken() : Boolean
+ownerAccepts(owner : Owner) : Boolean
+nameFree(name : String) : Boolean
}
class GiteaClient <<facade>> {
+GiteaClient(configuration : Configuration)
+hasLicense(key : String) : Boolean
+createRepository(request : ProjectRequest, license : String [0..1]) : GiteaRepository
+addPushMirror(source : GiteaRepository, target : GitHubRepository) : PushMirror
-requestSync(mirror : PushMirror) : void
}
class GitHubClient <<facade>> {
+GitHubClient(configuration : Configuration)
+createEmptyRepository(request : ProjectRequest) : GitHubRepository
}
class LocalProjectBuilder {
+build(directory : Path, source : GiteaRepository, target : GitHubRepository [0..1], sshPassed : Boolean) : LocalProject
}
class FrameworkInstaller {
+install(project : LocalProject, enablePlanGate : Boolean) : InstallResult
}
class SummaryReport {
+compose(request : ProjectRequest) : Summary
}
class Run {
-isApply : Boolean
}
class Configuration {
-giteaUrl : String
-giteaApiUrl : String
-githubWebUrl : String
-githubApiUrl : String
-giteaSshPort : Integer
-mirrorInterval : String
-frameworkRepo : String
-presetDetails : Map [0..1]
}
class Credential {
-kind : String
-value : String
}
class ToolCheck {
-hasGit : Boolean
-hasCurl : Boolean
-hasJq : Boolean
}
class PromptSet {
-prompts : String [1..*]
}
class ProjectRequest {
-name : String
-description : String
-visibility : Visibility
-directory : Path
-enablePlanGate : Boolean
}
class Owner {
-name : String
-kind : String
}
class PreflightResult {
-tokensWork : Boolean
-ownersAccept : Boolean
-nameIsFree : Boolean
-licenseIsOffered : Boolean
-sshPassed : Boolean
}
abstract class Repository {
-name : String
-description : String
-visibility : Visibility
-address : String
}
class GiteaRepository
class GitHubRepository
class LicenseFile {
-key : String
}
class PushMirror {
-interval : String
-syncOnCommit : Boolean
}
class LocalProject {
-directory : Path
}
class Remote {
-name : String
-address : String
}
class Submodule {
-name : String
-address : String
}
class HookSetup {
-areSkillsInstalled : Boolean
-areHooksInstalled : Boolean
-isPlanGateEnabled : Boolean
}
class EnvFile {
-address : Path
-keys : String [1..3]
}
class Template {
-name : String
-isCopied : Boolean
}
class InstallResult <<dto>>
class Summary {
-createdItems : String [0..*]
-skippedItems : String [0..*]
-nextSteps : String [0..*]
}
ProjectCreator ..> ConfigLoader : creates
ProjectCreator ..> ToolChecker : creates
ProjectCreator ..> CredentialCollector : creates
ProjectCreator ..> EnvFileWriter : creates [0..1]
ProjectCreator ..> Preflight : creates
ProjectCreator ..> GiteaClient : creates
ProjectCreator ..> GitHubClient : creates [0..1]
ProjectCreator ..> LocalProjectBuilder : creates
ProjectCreator ..> FrameworkInstaller : creates
ProjectCreator ..> SummaryReport : creates
Preflight ..> GiteaClient : asks
Preflight ..> GitHubClient : asks [0..1]
GitHost <|-- GiteaClient
GitHost <|-- GitHubClient
GitHost "1" --> "0..*" Owner : has
GiteaClient ..> Configuration
GitHubClient ..> Configuration
ProjectCreator "0..*" --> "1" Run
Run "1" *-- "1" Configuration
Run "1" *-- "1" ToolCheck
Run "1" *-- "0..1" ProjectRequest
Run "1" --> "1" PromptSet : returns
Configuration "1" *-- "1..3" Credential
ProjectRequest "0..*" --> "1" Owner : giteaOwner
ProjectRequest "0..*" --> "0..1" Owner : githubOwner
ProjectRequest "1" *-- "0..1" PreflightResult
ProjectRequest "1" --> "0..1" GiteaRepository : stored in
ProjectRequest "1" --> "0..1" GitHubRepository : also stored in
ProjectRequest "1" --> "0..1" LocalProject : working copy
Summary "0..*" --> "1" ProjectRequest : reports on
Repository <|-- GiteaRepository
Repository <|-- GitHubRepository
Repository "0..*" --> "1" Owner : owned by
GiteaRepository "1" *-- "0..1" LicenseFile
PushMirror "0..*" --> "1" GiteaRepository : source
PushMirror "0..*" --> "1" GitHubRepository : target
PushMirror "0..*" --> "1" Credential : authorised by
LocalProject "1" *-- "1..2" Remote
Remote "0..*" --> "1" Repository : points to
LocalProject "1" *-- "1" Submodule
LocalProject "1" *-- "1" HookSetup
LocalProject "1" *-- "0..*" Template
LocalProject "1" *-- "0..1" EnvFile
EnvFile "0..*" --> "1..3" Credential : copy of
InstallResult "0..*" --> "1" Submodule
InstallResult "0..*" --> "1" HookSetup
InstallResult "0..*" --> "0..*" Template
ProjectRequest "0..*" --> "1" Visibility
Repository "0..*" --> "1" Visibility
@enduml
```
## Class Table
| Class | Refines (Domain Model concept) | Responsibility | Attributes | Operations |
| --- | --- | --- | --- | --- |
| `ProjectCreator` | none (controller for the system operations of [OC-001]) | Receives the two system operations, sequences the steps and stops on the first failure. | none | `startProjectCreation`, `provideProjectDetails` |
| `ConfigLoader` | Configuration | Reads `config.env` and `.env` as plain text and validates every value, preset project details included. | none | `load` |
| `CredentialCollector` | none (system concept) | Asks, without echo, for a credential that `.env` does not provide and validates it like one read from `.env`. | none | `collect` |
| `EnvFileWriter` | Credentials File | Creates the project's own `.env` with the credentials the project needs: owner-only, excluded from git, never replaced without a yes. | none | `write` |
| `ToolChecker` | none (system concept `ToolCheck`) | Detects the required and optional tools. | none | `check` |
| `Preflight` | none (system concept `PreflightResult`) | Runs the read-only checks of both hosts before anything is created. | none | `check` |
| `GitHost` | Git Host | The operations every host offers: check the token, check that an owner accepts new repositories, check that a name is free. | `name`, `webAddress`, `apiAddress` | `verifyToken`, `ownerAccepts`, `nameFree` |
| `GiteaClient` | Git Host (Gitea) | Hides the Gitea API and its token; creates the repository and the push mirror. | none beyond `GitHost` (uses `Configuration`) | `GiteaClient`, `hasLicense`, `createRepository`, `addPushMirror`, `requestSync` |
| `GitHubClient` | Git Host (GitHub) | Hides the GitHub API and its token; creates the empty repository. | none beyond `GitHost` (uses `Configuration`) | `GitHubClient`, `createEmptyRepository` |
| `LocalProjectBuilder` | Local Project, Remote | Creates the project directory, its git repository and its credential-free remotes. | none | `build` |
| `FrameworkInstaller` | Framework, Framework Setup, Template | Adds the framework submodule, installs skills and hooks once, and copies the templates without overwriting. | none | `install` |
| `SummaryReport` | Summary | Composes the report of what was created, skipped or failed. | none | `compose` |
| `Run` | none (system concept) | Holds the state of one execution. | `isApply` | none |
| `Configuration` | Configuration | Holds the service addresses, the credentials and any preset project details. | `giteaUrl`, `giteaApiUrl`, `githubWebUrl`, `githubApiUrl`, `giteaSshPort`, `mirrorInterval`, `frameworkRepo`, `presetDetails` | none |
| `Credential` | Access Token | Holds a secret in memory only; it never becomes part of an address or a message. | `kind`, `value` | none |
| `ToolCheck` | none (system concept) | Records which tools are present. | `hasGit`, `hasCurl`, `hasJq` | none |
| `PromptSet` | none (system concept) | The questions still to ask; a detail preset in `config.env` is not in it. | `prompts` | none |
| `ProjectRequest` | Project | Holds the details of the project being created. | `name`, `description`, `visibility`, `directory`, `enablePlanGate` | none |
| `Owner` | Owner | A user or organization on a host. | `name`, `kind` | none |
| `PreflightResult` | none (system concept) | Records the outcome of the preflight checks. | `tokensWork`, `ownersAccept`, `nameIsFree`, `licenseIsOffered`, `sshPassed` | none |
| `Repository` | Repository | Common data of a repository on a host. | `name`, `description`, `visibility`, `address` | none |
| `GiteaRepository` | Gitea Repository | The source of truth. | none beyond `Repository` | none |
| `GitHubRepository` | GitHub Repository | Receives its content from the mirror. | none beyond `Repository` | none |
| `LicenseFile` | License | The `AGPL-3.0` file in the Gitea repository when GitHub is chosen. | `key` | none |
| `PushMirror` | Mirror | The Gitea to GitHub push mirror. | `interval`, `syncOnCommit` | none |
| `LocalProject` | Local Project | The project directory on the Maintainer's machine. | `directory` | none |
| `Remote` | Remote | A named link to a repository (`origin`, `github`), without a credential. | `name`, `address` | none |
| `Submodule` | Framework | The framework added to the local project. | `name`, `address` | none |
| `HookSetup` | Framework Setup | Records the skills and hooks installed and the plan gate state. | `areSkillsInstalled`, `areHooksInstalled`, `isPlanGateEnabled` | none |
| `EnvFile` | Credentials File | The `.env` of the project: a copy of the credentials it needs. | `address`, `keys` | none |
| `Template` | Template | A framework file copied into the project. | `name`, `isCopied` | none |
| `InstallResult` | none (carries the result of one operation) | Returns the submodule, the hook setup and the templates of `install`. | none | none |
| `Summary` | Summary | The report returned to the Maintainer; it contains no credential. | `createdItems`, `skippedItems`, `nextSteps` | none |
| `Visibility` | none (enumeration of a Project and Repository attribute) | The two allowed visibilities. | `private`, `public` | none |
## Method Traceability
| Method signature | Operation Contract / SD message |
| --- | --- |
| `ProjectCreator.startProjectCreation() : PromptSet` | [OC-001] `startProjectCreation`; [SD-001] `startProjectCreation()` |
| `ProjectCreator.provideProjectDetails(name, description, visibility, giteaOwner, githubOwner, directory, enablePlanGate, writeEnvFile) : Summary` | [OC-001] `provideProjectDetails`; [SD-001] `provideProjectDetails(...)` |
| `ConfigLoader.load(configFile, envFile) : Configuration` | [SD-001] `load(config.env, .env)`; [OC-001] `startProjectCreation` P2 |
| `CredentialCollector.collect(configuration, kinds) : Configuration` | [SD-001] `collect(configuration, GITEA_TOKEN)` and `collect(configuration, GITHUB_PAT, GITHUB_USER)`; [OC-001] `startProjectCreation` P2 and the precondition of `provideProjectDetails` |
| `EnvFileWriter.write(project, configuration, hasGithub) : EnvFile` | [SD-001] `write(localProject, configuration, githubOwner present)`; [OC-001] `provideProjectDetails` P14 |
| `ToolChecker.check(tools) : ToolCheck` | [SD-001] `check(git, curl, jq)`; [OC-001] `startProjectCreation` P3 |
| `Preflight.check(request) : PreflightResult` | [SD-001] `check(request)`; [OC-001] `provideProjectDetails` P2 |
| `GiteaClient(configuration)` | [SD-001] `new(configuration)` to `GiteaClient` |
| `GitHost.verifyToken() : Boolean` | [SD-001] `verifyToken()` from `Preflight` to either client; P2 |
| `GitHost.ownerAccepts(owner) : Boolean` | [SD-001] `ownerAccepts(giteaOwner)` and `ownerAccepts(githubOwner)`; P2 |
| `GitHost.nameFree(name) : Boolean` | [SD-001] `nameFree(name)` to either client; P2 |
| `GiteaClient.hasLicense(key) : Boolean` | [SD-001] `hasLicense(AGPL-3.0)`; P2 |
| `GiteaClient.createRepository(request, license) : GiteaRepository` | [SD-001] `createRepository(request, license)`; P3, P4 |
| `GiteaClient.addPushMirror(source, target) : PushMirror` | [SD-001] `addPushMirror(giteaRepository, gitHubRepository)`; P6 |
| `GiteaClient.requestSync(mirror) : void` | [SD-001] `requestSync(pushMirror)`; P6 |
| `GitHubClient(configuration)` | [SD-001] `new(configuration)` to `GitHubClient` |
| `GitHubClient.createEmptyRepository(request) : GitHubRepository` | [SD-001] `createEmptyRepository(request)`; P5 |
| `LocalProjectBuilder.build(directory, source, target, sshPassed) : LocalProject` | [SD-001] `build(directory, giteaRepository, gitHubRepository, sshPassed)`; P7, P8, P9 |
| `FrameworkInstaller.install(project, enablePlanGate) : InstallResult` | [SD-001] `install(localProject, enablePlanGate)`; P10, P11, P12 |
| `SummaryReport.compose(request) : Summary` | [SD-001] `compose(projectRequest)`; P13 |
## Pattern Annotations
| Pattern | Classes | Rationale |
| --- | --- | --- |
| Controller (GRASP) | `ProjectCreator` | One entry for the system operations; coordinates and does no HTTP, git or file work itself |
| Facade (GoF) | `GitHost`, `GiteaClient`, `GitHubClient` | Each client hides one host's HTTP API and keeps the token inside; no other class sees a credential. `GitHost` holds the operations both share |
| Pure Fabrication (GRASP) | `ConfigLoader`, `ToolChecker`, `CredentialCollector`, `EnvFileWriter`, `Preflight`, `LocalProjectBuilder`, `FrameworkInstaller`, `SummaryReport` | No domain concept owns these responsibilities; small units keep cohesion high |
| Creator (GRASP) | `ConfigLoader` creates `Configuration`; `GiteaClient` creates `GiteaRepository` and `PushMirror` | The creating class holds the data needed to build the object |
| Protection from variations (GRASP) | `GiteaClient`, `GitHubClient`, `ProjectRequest` | The optional GitHub path is decided by the controller; the clients do not know it |
| Data Transfer Object (GoF-style) | `InstallResult` | Carries the three results of `install` in one return value |
## Dependency Check
No circular dependency. `ProjectCreator` depends on every helper class and no helper depends on it. `Preflight` depends on the two clients; the clients extend `GitHost` and depend only on `Configuration`. The data classes form a tree: `Run` holds `Configuration`, `ToolCheck` and `ProjectRequest`; `ProjectRequest` reaches the repositories and the `LocalProject`; `Summary` points at `ProjectRequest` and nothing points back at it. `CredentialCollector` and `EnvFileWriter` depend only on `Configuration`, `Credential` and `LocalProject`; the only class that holds a secret after the run is `EnvFile`, and only as a copy written to the Maintainer's own disk. `Repository` is shared by `Remote` and `PushMirror` without a cycle.
SOLID check: no class has more than one reason to change (one host API, one kind of local work, one report); the clients can be replaced behind the same operations; the controller depends on the operations, not on how a host or git is called. `ProjectCreator` has two operations and no data, so it is not a god class.
## Implementation Mapping
| Design class | Where it lives in `src/` |
| --- | --- |
| `ProjectCreator` | `create-project.sh` (`main`), `lib/apply.sh` |
| `ConfigLoader` | `lib/config.sh` (`load_configuration`), `lib/validate.sh` |
| `ToolChecker` | `lib/tools.sh` |
| `CredentialCollector` | planned for [MIL-005]: `lib/credentials.sh`, with `lib/prompts.sh` |
| `EnvFileWriter` | planned for [MIL-005]: `lib/envfile.sh` |
| `Preflight` | `lib/preflight.sh` |
| `GitHost`, `GiteaClient`, `GitHubClient` | `lib/api.sh`, `lib/http.sh`, `lib/json.sh`, `lib/repositories.sh`, `lib/mirror.sh`, `lib/hosts.sh` |
| `LocalProjectBuilder` | `lib/localproject.sh`, `lib/git.sh` |
| `FrameworkInstaller` | `lib/framework.sh` |
| `SummaryReport` | `lib/steps.sh`, `lib/plan.sh` |
| `PromptSet`, `ProjectRequest` | `lib/project.sh`, `lib/prompts.sh` |
| `Run`, `Configuration`, `Credential`, `PreflightResult` | the state arrays declared in `lib/constants.sh` |
---
[UC-001]: ./uc.md
[DM-001]: ./dm.md
[DM-002]: ../domain-model.md
[OC-001]: ./oc.md
[SD-001]: ./sd.md
[MIL-005]: ../milestones/mil-005-credentials.md
[DICT-001]: ../dictionary.md
[DCD-002]: ../dcd.md
[f4d611b]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/f4d611b77cc70b4686506d44bf8f439045d9e0d2
[ded26a6]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/ded26a658c666bf29d84093cb352e3635e07719b
+11 -3
View File
@@ -9,8 +9,8 @@
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Initial version | [02875ae] |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Configuration may hold preset project details | [2a6bb8e] |
| 2026-10-05 | Deprecated | Jens Tirsvad Nielsen | S02 | Configuration may hold preset project details | [2a6bb8e] |
| 2026-10-06 | Accepted | Jens Tirsvad Nielsen | S02 | Added Credentials File (a Local Project may have one) | [ded26a6] |
---
@@ -79,6 +79,9 @@ class "Framework Setup" as FrameworkSetup {
class Template {
name
}
class "Credentials File" as CredentialsFile {
address
}
class Summary {
created items
skipped items
@@ -108,6 +111,8 @@ LocalProject "1" --> "1" FrameworkSetup : has
FrameworkSetup "0..*" --> "1" Framework : is installed from
Framework "1" --> "1..*" Template : provides
LocalProject "1" --> "0..*" Template : contains a copy of
LocalProject "1" --> "0..1" CredentialsFile : has
CredentialsFile "1" --> "1..2" AccessToken : holds a copy of
Summary "1" --> "1" Project : reports on
@enduml
```
@@ -132,6 +137,7 @@ Summary "1" --> "1" Project : reports on
| Framework | The SQA-QC-Framework added to a Local Project | name, address | [UC-001] step 9 "framework submodule" |
| Framework Setup | The skills and git hooks installed from the Framework, with the plan gate on or off | plan gate enabled | [UC-001] step 9 "skills and hooks", "plan gate" |
| Template | A file the Framework provides to copy into a project (`AGENTS.md`, artifact registry) | name | [UC-001] step 9 "templates" |
| Credentials File | The file in a Local Project that holds a copy of the Access Tokens (and the GitHub account name) the project needs; readable by its owner only and ignored by git | address | [UC-001] step 9 "credentials file" |
| Summary | The report of what was created, skipped or failed and how to continue | created items, skipped items, next steps | [UC-001] step 10 "summary" |
## Association Table
@@ -158,6 +164,8 @@ Summary "1" --> "1" Project : reports on
| Framework Setup | is installed from | Framework | 0..* to 1 |
| Framework | provides | Template | 1 to 1..* |
| Local Project | contains a copy of | Template | 1 to 0..* |
| Local Project | has | Credentials File | 1 to 0..1 |
| Credentials File | holds a copy of | Access Token | 1 to 1..2 |
| Summary | reports on | Project | 1 to 1 |
## Generalizations
@@ -172,5 +180,5 @@ Summary "1" --> "1" Project : reports on
[SSD-001]: ./ssd.md
[DICT-001]: ../dictionary.md
[DM-002]: ../domain-model.md
[02875ae]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/02875aee5f2953473924074eea0056eb31af6b7a
[2a6bb8e]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/2a6bb8e8afadfe6ca4a621da30e44a372898ca62
[ded26a6]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/ded26a658c666bf29d84093cb352e3635e07719b
+11 -7
View File
@@ -9,8 +9,8 @@
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Initial version | [02875ae] |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Project details may be defined by the Configuration | [2a6bb8e] |
| 2026-10-05 | Deprecated | Jens Tirsvad Nielsen | S02 | Project details may be defined by the Configuration | [2a6bb8e] |
| 2026-10-06 | Accepted | Jens Tirsvad Nielsen | S02 | Credentials asked when missing; EnvFile created in the local project (P14); writeEnvFile parameter | [ded26a6] |
---
@@ -31,7 +31,7 @@ Concepts below use the IT terms of [DICT-001] for the PO concepts of [DM-001]. `
**Postconditions**
- P1. A `Run` instance was created.
- P2. A `Configuration` instance was created from `config.env` and `.env`, with every value validated (including the project details preset in `config.env`) and the credentials held only in memory.
- P2. A `Configuration` instance was created from `config.env` and `.env`, with every value validated (including the project details preset in `config.env`). A `Credential` that `.env` did not provide was entered by the Maintainer without echo and validated; every `Credential` is held only in memory.
- P3. A `ToolCheck` instance was created and associated with the `Run`, recording that `git` and `curl` are present and whether `jq` is present.
- P4. The `Run` was associated with a `PromptSet` that is returned.
@@ -39,21 +39,23 @@ Concepts below use the IT terms of [DICT-001] for the PO concepts of [DM-001]. `
| Condition (failing precondition) | Outcome |
| --- | --- |
| `config.env` or `.env` is missing, or a value is missing or malformed (a preset project detail included) | The `Run` ends with an error naming the key, never its value; nothing was changed |
| `config.env` is missing, or a value in `config.env` or `.env` is malformed (a preset project detail included) | The `Run` ends with an error naming the key, never its value; nothing was changed |
| A `Credential` is missing and input ends before a valid one is entered | The `Run` ends with an error naming the key; nothing was changed |
| `git` or `curl` is missing | The `Run` ends with an error naming the tool; nothing was changed |
## Contract: provideProjectDetails
| Item | Value |
| --- | --- |
| Operation | `provideProjectDetails(name: String, description: String, visibility: Visibility, giteaOwner: Owner, githubOwner: Owner [0..1], directory: Path, enablePlanGate: Boolean): Summary` |
| Operation | `provideProjectDetails(name: String, description: String, visibility: Visibility, giteaOwner: Owner, githubOwner: Owner [0..1], directory: Path, enablePlanGate: Boolean, writeEnvFile: Boolean): Summary` |
| Traces to | `provideProjectDetails` in [SSD-001] |
| Concepts | ProjectRequest, PreflightResult, GiteaRepository, GitHubRepository, LicenseFile, PushMirror, LocalProject, Remote, Submodule, HookSetup, Summary |
| Concepts | ProjectRequest, PreflightResult, GiteaRepository, GitHubRepository, LicenseFile, PushMirror, LocalProject, Remote, Submodule, HookSetup, EnvFile, Summary |
**Preconditions**
- A `Run` exists and its `Configuration` is valid (from `startProjectCreation`).
- `githubOwner` is present exactly when the Maintainer chose GitHub.
- When `githubOwner` is present, the GitHub `Credential`s are known: from `.env`, or entered by the Maintainer without echo and validated before the first request.
- A detail that the `Configuration` defines is not asked: it is taken from the `Configuration`.
**Postconditions**
@@ -71,6 +73,7 @@ Concepts below use the IT terms of [DICT-001] for the PO concepts of [DM-001]. `
- P11. A `HookSetup` instance was associated with the `LocalProject`, recording that skills and git hooks were installed once and, if `enablePlanGate`, that the plan gate was enabled.
- P12. `AGENTS.md` and `docs/artifact-registry.md` exist in the `LocalProject`, each either newly copied from the framework templates or left as it was because the Maintainer declined to replace it.
- P13. A `Summary` instance was created listing every created item, every skipped item and the next step for anything that failed, and is returned. It contains no credential.
- P14. If `writeEnvFile`, an `EnvFile` named `.env` was associated with the `LocalProject`, holding only the `Credential`s the project needs (the Gitea token, and the GitHub token and account name when `githubOwner` is present). It is readable by its owner only and excluded from git without a change to any tracked file, and no `Credential` is shown in any output. If `writeEnvFile` is false, no `EnvFile` was created. An existing `.env` is left as it was unless the Maintainer agreed to replace it.
**Exceptions**
@@ -81,6 +84,7 @@ Concepts below use the IT terms of [DICT-001] for the PO concepts of [DM-001]. `
| `GiteaRepository` creation fails after a `GitHubRepository` was created (P5, P3 ordering) | The `Summary` lists the `GitHubRepository` as created, the `GiteaRepository` as failed and how to continue |
| `PushMirror` creation fails (P6) | The `Summary` lists both repositories as created, the mirror as failed and how to continue; the local steps are not run |
| `directory` exists, or a target file exists, and the Maintainer declines replacing it (P7, P12) | That item is skipped and listed in the `Summary` |
| A `.env` already exists in the `LocalProject` and the Maintainer declines replacing it (P14) | That item is skipped and listed in the `Summary` |
| A different `core.hooksPath` exists and the Maintainer declines replacing it (P11) | Hooks are not installed and this is listed in the `Summary` |
| SSH to port 10022 fails and the `Submodule` cannot be added (P10) | The `Summary` lists the repositories as created, the submodule as failed, and the SSH prerequisite |
@@ -90,5 +94,5 @@ Concepts below use the IT terms of [DICT-001] for the PO concepts of [DM-001]. `
[DM-001]: ./dm.md
[DICT-001]: ../dictionary.md
[SD-001]: ./sd.md
[02875ae]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/02875aee5f2953473924074eea0056eb31af6b7a
[2a6bb8e]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/2a6bb8e8afadfe6ca4a621da30e44a372898ca62
[ded26a6]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/ded26a658c666bf29d84093cb352e3635e07719b
+51 -22
View File
@@ -4,17 +4,17 @@
| Key | Value |
| --- | --- |
| ID | SD-001 |
| CrossReference | [OC-001] |
| CrossReference | [OC-001], [DCD-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Initial version | [02875ae] |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Note: preset project details are read by ConfigLoader | [2a6bb8e] |
| 2026-10-06 | Deprecated | Jens Tirsvad Nielsen | S02 | Messages aligned with the method signatures of DCD-001<br>Cited DCD-001 | [f4d611b] |
| 2026-10-06 | Accepted | Jens Tirsvad Nielsen | S02 | Added CredentialCollector and EnvFileWriter and their messages (P2, P14) | [ded26a6] |
---
Design objects are conceptual; in `create-project.sh` each becomes a small function group. No Design Class Diagram exists yet.
Design objects are conceptual; in `create-project.sh` each becomes a small function group. [DCD-001] gives each object its class and turns each message below into a method signature.
## Sequence: startProjectCreation
@@ -28,6 +28,7 @@ actor Maintainer
participant ":ProjectCreator" as PC
participant ":ConfigLoader" as CL
participant ":ToolChecker" as TC
participant ":CredentialCollector" as CC
Maintainer -> PC : startProjectCreation()
activate PC
@@ -36,6 +37,11 @@ PC -> CL : load(config.env, .env)
activate CL
CL --> PC : configuration
deactivate CL
create CC
PC -> CC : collect(configuration, GITEA_TOKEN)
activate CC
CC --> PC : configuration
deactivate CC
create TC
PC -> TC : check(git, curl, jq)
activate TC
@@ -44,6 +50,7 @@ deactivate TC
PC --> Maintainer : promptSet
deactivate PC
destroy CL
destroy CC
destroy TC
@enduml
```
@@ -53,7 +60,7 @@ destroy TC
| Pattern (GRASP / GoF) | Applied to | Rationale |
| --- | --- | --- |
| Controller (GRASP) | `ProjectCreator` | Receives the system operations and coordinates, without doing the work itself |
| Pure Fabrication (GRASP) | `ConfigLoader`, `ToolChecker` | No domain concept owns parsing or tool checks; separate small units keep cohesion high |
| Pure Fabrication (GRASP) | `ConfigLoader`, `ToolChecker`, `CredentialCollector` | No domain concept owns parsing, tool checks or asking for a credential; separate small units keep cohesion high |
| Creator (GRASP) | `ConfigLoader` creates `Configuration` | It holds the data needed to build and validate it |
### Postcondition Coverage
@@ -61,13 +68,13 @@ destroy TC
| Postcondition (from contract) | Satisfied by message |
| --- | --- |
| P1 Run created | `startProjectCreation` received by `ProjectCreator` |
| P2 Configuration created and validated | `load(config.env, .env)` |
| P2 Configuration created and validated; a missing credential entered, validated and held in memory | `load(config.env, .env)` and `collect(configuration, GITEA_TOKEN)` |
| P3 ToolCheck created | `check(git, curl, jq)` |
| P4 PromptSet returned | `promptSet` return to the Maintainer |
### Responsibility Check
`ProjectCreator` only sequences two calls; parsing and validation sit in `ConfigLoader`, tool detection in `ToolChecker`. No object receives every message. Project details preset in `config.env` are read and validated by `ConfigLoader` as part of `configuration`; the second sequence is unchanged, because `provideProjectDetails` receives the same arguments whether they were asked or preset.
`ProjectCreator` only sequences three calls; parsing and validation sit in `ConfigLoader`, asking for a missing credential in `CredentialCollector`, tool detection in `ToolChecker`. No object receives every message. Project details preset in `config.env` are read and validated by `ConfigLoader` as part of `configuration`; the second sequence is unchanged, because `provideProjectDetails` receives the same arguments whether they were asked or preset.
## Sequence: provideProjectDetails
@@ -85,8 +92,10 @@ participant ":GitHubClient" as GH
participant ":LocalProjectBuilder" as LB
participant ":FrameworkInstaller" as FI
participant ":SummaryReport" as SR
participant ":CredentialCollector" as CC
participant ":EnvFileWriter" as EW
Maintainer -> PC : provideProjectDetails(name, description, visibility, giteaOwner, githubOwner, directory, enablePlanGate)
Maintainer -> PC : provideProjectDetails(name, description, visibility, giteaOwner, githubOwner, directory, enablePlanGate, writeEnvFile)
activate PC
create GT
PC -> GT : new(configuration)
@@ -94,6 +103,13 @@ opt githubOwner present
create GH
PC -> GH : new(configuration)
end
opt githubOwner present
create CC
PC -> CC : collect(configuration, GITHUB_PAT, GITHUB_USER)
activate CC
CC --> PC : configuration
deactivate CC
end
create PF
PC -> PF : check(request)
activate PF
@@ -105,16 +121,16 @@ PF --> PC : preflightResult
deactivate PF
opt githubOwner present
PC -> GH : createEmptyRepository(githubOwner, name)
PC -> GH : createEmptyRepository(request)
activate GH
GH --> PC : gitHubRepository
deactivate GH
end
alt githubOwner present
PC -> GT : createRepository(giteaOwner, name, license=AGPL-3.0)
PC -> GT : createRepository(request, license=AGPL-3.0)
else no GitHub
PC -> GT : createRepository(giteaOwner, name, license=none)
PC -> GT : createRepository(request, license=none)
end
activate GT
GT --> PC : giteaRepository
@@ -123,7 +139,7 @@ deactivate GT
opt githubOwner present
PC -> GT : addPushMirror(giteaRepository, gitHubRepository)
activate GT
GT -> GT : requestSync()
GT -> GT : requestSync(pushMirror)
GT --> PC : pushMirror
deactivate GT
end
@@ -137,11 +153,19 @@ deactivate LB
create FI
PC -> FI : install(localProject, enablePlanGate)
activate FI
FI --> PC : submodule, hookSetup, templates
FI --> PC : installResult
deactivate FI
opt writeEnvFile
create EW
PC -> EW : write(localProject, configuration, githubOwner present)
activate EW
EW --> PC : envFile
deactivate EW
end
create SR
PC -> SR : compose(all results)
PC -> SR : compose(request)
SR --> PC : summary
PC --> Maintainer : summary
deactivate PC
@@ -150,6 +174,8 @@ destroy GT
destroy GH
destroy LB
destroy FI
destroy CC
destroy EW
destroy SR
@enduml
```
@@ -159,7 +185,7 @@ destroy SR
| Pattern (GRASP / GoF) | Applied to | Rationale |
| --- | --- | --- |
| Controller (GRASP) | `ProjectCreator` | Single entry for the system operation; sequences the steps and stops on the first failure |
| Pure Fabrication (GRASP) | `Preflight`, `LocalProjectBuilder`, `FrameworkInstaller`, `SummaryReport` | Each groups one responsibility that no domain concept owns |
| Pure Fabrication (GRASP) | `Preflight`, `LocalProjectBuilder`, `FrameworkInstaller`, `SummaryReport`, `CredentialCollector`, `EnvFileWriter` | Each groups one responsibility that no domain concept owns |
| Facade (GoF) | `GiteaClient`, `GitHubClient` | Hide each host's HTTP API and credential handling behind a small interface; tokens never leave them |
| Protection from variations (GRASP) | Client classes | The `github`-optional and license variations are decided by the controller's `alt` and `opt`, not inside the clients |
@@ -169,24 +195,27 @@ destroy SR
| --- | --- |
| P1 ProjectRequest created | `provideProjectDetails` received by `ProjectCreator` |
| P2 PreflightResult created | `check(request)` |
| P3 GiteaRepository created | `createRepository(giteaOwner, name, license)` |
| P3 GiteaRepository created | `createRepository(request, license)` |
| P4 LicenseFile when GitHub chosen, otherwise empty | `createRepository(..., license=AGPL-3.0)` and the `alt` branch `license=none` |
| P5 empty GitHubRepository when chosen | `createEmptyRepository(githubOwner, name)` |
| P6 PushMirror and first sync | `addPushMirror(...)` and `requestSync()` |
| P2 GitHub credentials known before the first request | `collect(configuration, GITHUB_PAT, GITHUB_USER)` inside `opt githubOwner present` |
| P5 empty GitHubRepository when chosen | `createEmptyRepository(request)` |
| P6 PushMirror and first sync | `addPushMirror(...)` and `requestSync(pushMirror)` |
| P7 LocalProject created, history from Gitea when not empty | `build(directory, ...)` |
| P8 origin remote (SSH if the test passed, else HTTPS) | `build(..., sshPassed)` |
| P9 github remote when chosen | `build(...)` |
| P10 framework Submodule | `install(localProject, ...)` |
| P11 HookSetup, plan gate if chosen | `install(localProject, enablePlanGate)` |
| P12 AGENTS.md and registry copied or kept | `install(...)` returning `templates` |
| P13 Summary created and returned | `compose(all results)` and the final return |
| P13 Summary created and returned | `compose(request)` and the final return |
| P14 EnvFile created with the needed credentials, owner-only, ignored by git, or none when declined | `write(localProject, configuration, githubOwner present)` inside `opt writeEnvFile` |
### Responsibility Check
`ProjectCreator` sequences and decides on the optional paths but performs no HTTP, git or file work. Host calls are in the two clients, local work in `LocalProjectBuilder` and `FrameworkInstaller`, reporting in `SummaryReport`, so cohesion stays high and no object receives all messages. Failure handling (exceptions in [OC-001]) is the controller's single stop-and-report rule and is not drawn.
`ProjectCreator` sequences and decides on the optional paths but performs no HTTP, git or file work. Host calls are in the two clients, local work in `LocalProjectBuilder` and `FrameworkInstaller`, the credential prompts in `CredentialCollector`, the `.env` in `EnvFileWriter`, reporting in `SummaryReport`, so cohesion stays high and no object receives all messages. Failure handling (exceptions in [OC-001]) is the controller's single stop-and-report rule and is not drawn.
---
[OC-001]: ./oc.md
[02875ae]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/02875aee5f2953473924074eea0056eb31af6b7a
[2a6bb8e]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/2a6bb8e8afadfe6ca4a621da30e44a372898ca62
[DCD-001]: ./dcd.md
[f4d611b]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/f4d611b77cc70b4686506d44bf8f439045d9e0d2
[ded26a6]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/ded26a658c666bf29d84093cb352e3635e07719b
+7 -7
View File
@@ -9,8 +9,8 @@
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Optional GitHub; choosing GitHub applies the AGPL license to the Gitea repository<br>Cited OC-001 and DM-001 | [02875ae] |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Parameters may come from config.env; the message is unchanged | [2a6bb8e] |
| 2026-10-05 | Deprecated | Jens Tirsvad Nielsen | S02 | Parameters may come from config.env; the message is unchanged | [2a6bb8e] |
| 2026-10-06 | Accepted | Jens Tirsvad Nielsen | S02 | writeEnvFile parameter and the credentials that .env does not provide | [ded26a6] |
---
@@ -26,7 +26,7 @@ actor Maintainer as A
participant ":System" as S
A -> S : startProjectCreation()
S --> A : prompts for project details
A -> S : provideProjectDetails(name, description, visibility, giteaOwner, githubOwner, directory, enablePlanGate)
A -> S : provideProjectDetails(name, description, visibility, giteaOwner, githubOwner, directory, enablePlanGate, writeEnvFile)
S --> A : checks passed
S --> A : creation summary
@enduml
@@ -36,19 +36,19 @@ S --> A : creation summary
| Step | Message | Parameters | Return | Use case step |
| --- | --- | --- | --- | --- |
| 1 | startProjectCreation | none | prompts for project details (after configuration and tool checks) | 1, 2 |
| 2 | provideProjectDetails | name, description, visibility, giteaOwner, githubOwner (optional; given means GitHub is chosen and the Gitea repository gets the AGPL license; omitted means no GitHub and no license), directory, enablePlanGate (each of these may come from `config.env` instead of the Maintainer; the message is unchanged) | checks passed, then a creation summary | 3 to 10 |
| 1 | startProjectCreation | none | prompts for project details (after configuration and tool checks; a missing credential is asked first) | 1, 2 |
| 2 | provideProjectDetails | name, description, visibility, giteaOwner, githubOwner (optional; given means GitHub is chosen and the Gitea repository gets the AGPL license; omitted means no GitHub and no license), directory, enablePlanGate (each of these may come from `config.env` instead of the Maintainer; the message is unchanged), writeEnvFile (whether to create the project's `.env`), and the credentials that `.env` does not provide (GitHub ones only when GitHub is chosen) | checks passed, then a creation summary | 3 to 10 |
Steps 4 to 9 are internal to the system, so one operation covers them. A consent question (step 8a, 9a, 9b) is a prompt from the system and is out of scope for this diagram; failure flows are out of scope here.
## Lifecycle Notes
The system is one script run. It starts with the first operation and ends after the summary; nothing persists between runs.
The system is one script run. It starts with the first operation and ends after the summary; nothing persists between runs except the `.env` the Maintainer agreed to in the new project.
---
[UC-001]: ./uc.md
[DM-001]: ./dm.md
[OC-001]: ./oc.md
[02875ae]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/02875aee5f2953473924074eea0056eb31af6b7a
[2a6bb8e]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/2a6bb8e8afadfe6ca4a621da30e44a372898ca62
[ded26a6]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/ded26a658c666bf29d84093cb352e3635e07719b
+16 -7
View File
@@ -9,8 +9,8 @@
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Optional GitHub; choosing GitHub applies the AGPL license to the Gitea repository<br>Cited DM-001 and UCD-001 | [02875ae] |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Step 3: details set in config.env are not asked (extensions 3a, 3b) | [2a6bb8e] |
| 2026-10-05 | Deprecated | Jens Tirsvad Nielsen | S02 | Step 3: details set in config.env are not asked (extensions 3a, 3b) | [2a6bb8e] |
| 2026-10-06 | Accepted | Jens Tirsvad Nielsen | S02 | Credentials not in .env are asked (step 2, extension 2b); the project .env is created with consent (step 9, extensions 9c, 9d) | [ded26a6] |
---
@@ -26,33 +26,36 @@
- S02 — credentials are never exposed and nothing is overwritten silently
- S03 — the published procedure is documented and reusable
- **Preconditions:**
- `config.env` and `.env` exist and are valid.
- `config.env` exists and is valid. `.env` may be missing or hold only some credentials; a credential it does not provide is asked.
- `config.env` may preset any of the project details of step 3.
- `git` and `curl` are installed.
- The Maintainer has a Gitea token, a GitHub PAT (only when GitHub is chosen) and SSH access to Gitea on port 10022.
- The Maintainer has a Gitea token, a GitHub PAT and a GitHub account name (only when GitHub is chosen) and SSH access to Gitea on port 10022.
- **Postconditions (success guarantee):**
- A repository exists on Gitea under the chosen owner. It is empty, or, when the Maintainer chose GitHub, it holds the AGPL license file.
- When the Maintainer chose to create a GitHub repository, an empty repository exists on GitHub under the chosen owner, the Gitea repository is a push mirror to it, and the AGPL license file reaches GitHub through the mirror.
- A local project directory exists with credential-free remotes `origin` (Gitea) and, when GitHub was chosen, `github`, the `framework` submodule, installed skills and hooks, and the copied templates.
- When the Maintainer agreed, the local project has a `.env` that holds only the credentials the project needs, is readable by its owner only and is ignored by git.
- The Maintainer has a summary of what was created.
### Main Success Scenario
1. The Maintainer starts the project creation.
2. The system loads and validates the configuration and credentials and checks that the required tools exist.
2. The system loads and validates the configuration and credentials and checks that the required tools exist. A credential that `.env` does not provide is asked, without echo; the GitHub credentials are asked once GitHub is chosen.
3. The Maintainer provides the repository name, description, visibility, the Gitea owner, whether to also create a GitHub repository (and if so its owner), the local directory, and whether to enable the plan gate. A detail that is set in `config.env` is not asked.
4. The system checks that the tokens needed for the chosen hosts work, that the owners accept new repositories, that the name is free on those hosts, and whether SSH to Gitea works.
5. Optional: if the Maintainer chose GitHub, the system creates the empty GitHub repository.
6. The system creates the Gitea repository. If the Maintainer chose GitHub, the repository is created with the AGPL license file and so is not empty; otherwise it is empty and has no license.
7. Optional: if GitHub was chosen, the system configures the Gitea repository as a push mirror to GitHub and verifies it. A license file in the Gitea repository is pushed to GitHub by the mirror.
8. The system creates the local project with the `origin` remote and, if GitHub was chosen, the `github` remote.
9. The system adds the framework submodule, installs its skills and hooks (and the plan gate if chosen) and copies the templates.
9. The system adds the framework submodule, installs its skills and hooks (and the plan gate if chosen) and copies the templates. If the Maintainer agrees, it also creates the project's own `.env` with the credentials the project needs.
10. The system reports a summary of what was created.
### Extensions (Alternative / Exception Flows)
- 2a. A required tool is missing, or a configuration value is missing or malformed:
1. The system stops before any change and names the problem without showing a credential.
- 2b. A credential is not provided in `.env`:
1. The system asks for it without showing what is typed. An invalid value is refused and asked again; when input ends the system stops before any change and names the key.
- 3a. A project detail is set in `config.env`:
1. The system uses it and does not ask for it; the summary says it came from the configuration.
- 3b. A configured project detail is invalid:
@@ -69,12 +72,18 @@
1. The system asks the Maintainer before replacing it; on no, it skips that item and reports it.
- 9b. A different git hooks setup is already configured in the project:
1. The system asks before replacing it.
- 9c. The Maintainer declines creating the project's `.env`:
1. The system creates none and says so in the summary.
- 9d. A `.env` already exists in the project:
1. The system asks before replacing it; on no, it keeps it and reports it.
### Special Requirements / Business Rules
| Step | Rule |
| --- | --- |
| 2, 4 | A token never appears in output, logs, command lines, remote URLs or temporary files left behind |
| 2 | A credential that is asked is read without echo, validated like one read from `.env`, and held in memory for the run |
| 9 | The project's `.env` is the only place a token is written. It is created only after a yes (default no), holds only the keys the project needs (`GITEA_TOKEN`; `GITHUB_PAT` and `GITHUB_USER` when GitHub was chosen), is readable by its owner only, is excluded from git without changing a tracked file, and is never replaced without a yes |
| 3 | The GitHub owner and the Gitea owner are chosen separately; `GITHUB_USER` is only the authenticating account |
| 3 | A project detail set in `config.env` (the key is present, even if empty where an empty value is allowed) is not asked; only the confirmations stay interactive |
| 3, 5, 7 | GitHub is optional; without it no GitHub repository, mirror or `github` remote is created and the GitHub credentials are not required |
@@ -93,5 +102,5 @@
[US-001]: ../user-stories.md
[SA-001]: ../stakeholder-analysis.md
[DM-001]: ./dm.md
[02875ae]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/02875aee5f2953473924074eea0056eb31af6b7a
[2a6bb8e]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/2a6bb8e8afadfe6ca4a621da30e44a372898ca62
[ded26a6]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/ded26a658c666bf29d84093cb352e3635e07719b
+23 -6
View File
@@ -4,13 +4,13 @@
| Key | Value |
| --- | --- |
| ID | US-001 |
| CrossReference | [BC-001], [UCD-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004] |
| CrossReference | [BC-001], [UCD-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Optional GitHub; choosing GitHub applies the AGPL license to the Gitea repository<br>Cited UCD-001<br>Split the epic into three stories, one per milestone | [02875ae] |
| 2026-10-05 | Accepted | Jens Tirsvad Nielsen | S02 | Added US-001.04: project details preset in config.env | [2a6bb8e] |
| 2026-10-05 | Deprecated | Jens Tirsvad Nielsen | S02 | Added US-001.04: project details preset in config.env | [2a6bb8e] |
| 2026-10-06 | Accepted | Jens Tirsvad Nielsen | S02 | Added US-001.05: credentials asked when missing and kept in the project .env | [ded26a6] |
---
@@ -18,7 +18,7 @@
One epic: "Create a new project" ([UC-001]), setting up a new project on Gitea, optionally on GitHub, with the SQA-QC-Framework in place. The actor is the Maintainer, as in [UCD-001] (S01 or S02; for now one person holds both roles).
The epic is split into four stories, one per milestone. Each story fits one two-week phase and can be shown working at the end of it.
The epic is split into five stories, one per milestone. Each story fits one two-week phase and can be shown working at the end of it.
## Story List
@@ -79,9 +79,25 @@ The epic is split into four stories, one per milestone. Each story fits one two-
| --- | --- | --- |
| [UC-001] step 3, [MIL-004] | fits one phase | Independent: needs the prompts of US-001.01 |
### US-001.05 — Create a new project: ask for the credentials and keep them in the project
**As a** Maintainer, **I want** the script to ask for a credential that `.env` does not provide and to create a `.env` file in the new project, **so that** I can start without a prepared `.env` and the new project has the credentials its tools need.
**Acceptance Criteria**
- Given `GITEA_TOKEN` is not provided in `.env`, when the script starts, then it asks for it without showing what is typed and does not stop with an error; the same holds for `GITHUB_PAT` and `GITHUB_USER` when GitHub is chosen.
- Given an entered credential is not valid, when the script checks it, then it asks again and never shows the value.
- Given the project exists, when the Maintainer agrees, then the new project has a `.env` that holds only the credentials the project needs, is readable by its owner only and is ignored by git.
- Given the Maintainer declines, or `.env` already exists in the project and the Maintainer declines replacing it, then no `.env` is written or replaced and the summary says so.
- Given any run, then no credential appears in output, remotes, tracked files or the summary.
| Traces to | Size | INVEST exceptions |
| --- | --- | --- |
| [UC-001] steps 2 and 9, [MIL-005] | fits one phase | Independent: needs the local project of US-001.03 |
## INVEST Check
Valuable, Negotiable, Estimable, Small and Testable hold for each story. Independent holds only in part: the stories are ordered, each using what the one before it delivers, which follows the milestone order in [PP-001]. This is flagged as an exception on US-001.02, US-001.03 and US-001.04.
Valuable, Negotiable, Estimable, Small and Testable hold for each story. Independent holds only in part: the stories are ordered, each using what the one before it delivers, which follows the milestone order in [PP-001]. This is flagged as an exception on US-001.02 to US-001.05.
---
@@ -92,6 +108,7 @@ Valuable, Negotiable, Estimable, Small and Testable hold for each story. Indepen
[MIL-002]: ./milestones/mil-002-repositories-and-mirror.md
[MIL-003]: ./milestones/mil-003-scaffold-and-release.md
[MIL-004]: ./milestones/mil-004-configurable-details.md
[MIL-005]: ./milestones/mil-005-credentials.md
[PP-001]: ./project-plan.md
[02875ae]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/02875aee5f2953473924074eea0056eb31af6b7a
[2a6bb8e]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/2a6bb8e8afadfe6ca4a621da30e44a372898ca62
[ded26a6]: https://git.tirsystem.com/TirSystem-BashScript/repo_foundry/commit/ded26a658c666bf29d84093cb352e3635e07719b
+15 -6
View File
@@ -11,7 +11,9 @@
# repository, remotes (no credential in any address), the framework as a
# submodule, the framework's skills and git hooks (and the plan gate if
# chosen) and its templates. Choosing GitHub also applies the AGPL-3.0
# license to the Gitea repository. No commit is made in the new project.
# license to the Gitea repository. After a yes (default no) it also writes
# the new project's own .env with the credentials the project needs. No
# commit is made in the new project.
#
# Dry run by default
# Without --apply the script only reads from GitHub and Gitea (GET
@@ -27,7 +29,8 @@
# Options
# --apply create the repositories and the mirror (after a final yes)
# --config FILE service addresses (default: config.env in the project root)
# --env FILE credentials (default: .env in the project root)
# --env FILE credentials (default: .env in the project root); optional:
# a credential it does not provide is asked, not echoed
# -h, --help show this help
# --version show the version
#
@@ -36,7 +39,8 @@
# the optional GITEA_SSH_PORT (default 10022), MIRROR_INTERVAL
# (default 10m0s) and FRAMEWORK_REPO (default
# TirSystem/SQA-QC-Framework, the submodule's OWNER/NAME)
# .env GITHUB_PAT, GITHUB_USER, GITEA_TOKEN
# .env GITHUB_PAT, GITHUB_USER, GITEA_TOKEN (all optional, each
# asked when missing)
#
# Environment
# REPOFOUNDRY_NAME project name used in messages (default: RepoFoundry)
@@ -65,8 +69,8 @@
# This file is the entry point. The work is split by responsibility into
# the files in lib/ next to it (one job per file, see the first lines of
# each file): constants, output, temp, util, validate, config, tools, json,
# http, api, prompts, project, hosts, preflight, steps, plan, repositories,
# mirror, git, localproject, framework, apply and cli. The files are loaded
# http, api, prompts, credentials, project, hosts, preflight, steps, plan,
# repositories, mirror, git, localproject, framework, envfile, apply and cli. The files are loaded
# from this directory only.
#
# Exit codes
@@ -121,6 +125,8 @@ source "$SCRIPT_DIR/lib/http.sh"
source "$SCRIPT_DIR/lib/api.sh"
# shellcheck source=lib/prompts.sh
source "$SCRIPT_DIR/lib/prompts.sh"
# shellcheck source=lib/credentials.sh
source "$SCRIPT_DIR/lib/credentials.sh"
# shellcheck source=lib/project.sh
source "$SCRIPT_DIR/lib/project.sh"
# shellcheck source=lib/hosts.sh
@@ -141,6 +147,8 @@ source "$SCRIPT_DIR/lib/git.sh"
source "$SCRIPT_DIR/lib/localproject.sh"
# shellcheck source=lib/framework.sh
source "$SCRIPT_DIR/lib/framework.sh"
# shellcheck source=lib/envfile.sh
source "$SCRIPT_DIR/lib/envfile.sh"
# shellcheck source=lib/apply.sh
source "$SCRIPT_DIR/lib/apply.sh"
# shellcheck source=lib/cli.sh
@@ -166,9 +174,10 @@ main() {
check_tools
setup_temp_dir
load_configuration
collect_credentials GITEA_TOKEN
collect_project_details
if ((PROJECT[has_github])); then
require_github_credentials
collect_credentials GITHUB_PAT GITHUB_USER
fi
init_steps
print_summary
+1
View File
@@ -19,6 +19,7 @@ create_all() {
add_framework
install_framework
copy_templates
create_env_file
}
# confirm_framework_access: the framework comes over SSH. Without SSH the
+16 -20
View File
@@ -4,7 +4,7 @@
#
# Part of create-project.sh: sourced by it, never run on its own.
#
# Provides: unquote_value, parse_env_file, parse_env_entry, validate_config, check_preset, check_preset_choice, validate_project_presets, validate_credentials, require_github_credentials, warn_if_env_unsafe, load_configuration
# Provides: unquote_value, parse_env_file, parse_env_entry, validate_config, check_preset, check_preset_choice, validate_project_presets, validate_credentials, warn_if_env_unsafe, load_configuration
# unquote_value RAW: strip matching quotes (or a trailing " # comment" on an
# unquoted value) and return the value in REPLY. Fails on unbalanced quotes.
@@ -144,20 +144,23 @@ validate_project_presets() {
check_preset_choice ENABLE_PLAN_GATE yes no
}
# validate_credentials: check the credentials that .env provides. A credential
# that is not provided is not an error: it is asked later (collect_credentials).
validate_credentials() {
if [[ -z ${CREDENTIALS[GITEA_TOKEN]:-} ]]; then
die "GITEA_TOKEN is missing in $ENV_FILE (see .env.example)"
fi
# Register secrets first so that no later message can show them.
SECRET_VALUES+=("${CREDENTIALS[GITEA_TOKEN]}")
if [[ -n ${CREDENTIALS[GITEA_TOKEN]:-} ]]; then
SECRET_VALUES+=("${CREDENTIALS[GITEA_TOKEN]}")
fi
if [[ -n ${CREDENTIALS[GITHUB_PAT]:-} ]]; then
SECRET_VALUES+=("${CREDENTIALS[GITHUB_PAT]}")
fi
is_valid_token "${CREDENTIALS[GITEA_TOKEN]}" ||
die "GITEA_TOKEN in $ENV_FILE is not a valid token (8 to 255 letters, digits or _ . ~ + / = -)"
if [[ -n ${CREDENTIALS[GITEA_TOKEN]:-} ]] &&
! is_valid_token "${CREDENTIALS[GITEA_TOKEN]}"; then
die "GITEA_TOKEN in $ENV_FILE is not a valid token ($HINT_TOKEN)"
fi
if [[ -n ${CREDENTIALS[GITHUB_PAT]:-} ]] &&
! is_valid_token "${CREDENTIALS[GITHUB_PAT]}"; then
die "GITHUB_PAT in $ENV_FILE is not a valid token (8 to 255 letters, digits or _ . ~ + / = -)"
die "GITHUB_PAT in $ENV_FILE is not a valid token ($HINT_TOKEN)"
fi
if [[ -n ${CREDENTIALS[GITHUB_USER]:-} ]] &&
! is_valid_github_owner "${CREDENTIALS[GITHUB_USER]}"; then
@@ -165,16 +168,6 @@ validate_credentials() {
fi
}
# GitHub credentials are only needed when the Maintainer chose GitHub.
require_github_credentials() {
local key
for key in GITHUB_PAT GITHUB_USER; do
if [[ -z ${CREDENTIALS[$key]:-} ]]; then
die "GitHub was chosen but $key is missing in $ENV_FILE (see .env.example)"
fi
done
}
warn_if_env_unsafe() {
local file="$1" dir mode
case "$(uname -s 2>/dev/null || true)" in
@@ -200,7 +193,10 @@ warn_if_env_unsafe() {
load_configuration() {
parse_env_file "$CONFIG_FILE" CONFIG_KEYS CONFIG
validate_config
parse_env_file "$ENV_FILE" CREDENTIAL_KEYS CREDENTIALS
# .env is optional: a credential it does not provide is asked.
if [[ -e $ENV_FILE ]]; then
parse_env_file "$ENV_FILE" CREDENTIAL_KEYS CREDENTIALS
warn_if_env_unsafe "$ENV_FILE"
fi
validate_credentials
warn_if_env_unsafe "$ENV_FILE"
}
+3 -1
View File
@@ -26,8 +26,10 @@ readonly HINT_DESCRIPTION="at most $MAX_DESCRIPTION_LENGTH characters and no con
readonly HINT_GITEA_OWNER="use letters, digits, '.', '_' or '-' (at most 39)"
readonly HINT_GITHUB_OWNER="use letters, digits or '-' (at most 39)"
readonly HINT_DIRECTORY="must not be empty, start with '-' or contain control characters"
readonly HINT_TOKEN="8 to 255 letters, digits or _ . ~ + / = -"
readonly ENV_FILE_NAME=".env"
readonly PLAN_STEPS=("GitHub repository" "Gitea repository" "Push mirror"
"Local project" "Framework" "Skills and hooks" "Templates")
"Local project" "Framework" "Skills and hooks" "Templates" "Project .env")
# shellcheck disable=SC2034 # read through namerefs (parse_env_file)
readonly CONFIG_KEYS=(GITHUB_API_URL GITHUB_WEB_URL GITEA_URL GITEA_API_URL
GITEA_SSH_PORT MIRROR_INTERVAL FRAMEWORK_REPO
+42
View File
@@ -0,0 +1,42 @@
# shellcheck shell=bash
# shellcheck disable=SC2004,SC2034,SC2154 # shared state and arrays are declared in constants.sh
# credentials.sh - Asking for a credential that .env does not provide.
#
# Part of create-project.sh: sourced by it, never run on its own.
#
# Provides: credential_label, collect_credentials
# credential_label KEY: the name of a credential as the Maintainer sees it.
credential_label() {
case "$1" in
GITEA_TOKEN) printf 'Gitea access token' ;;
GITHUB_PAT) printf 'GitHub personal access token' ;;
GITHUB_USER) printf 'GitHub account name (the account the token belongs to)' ;;
*) printf '%s' "$1" ;;
esac
}
# collect_credentials KEY...: ask for each credential that is not already
# provided. A token is read without echo and registered as a secret at once,
# so no later message can show it; the GitHub account name is not secret and
# is read like any other answer. An empty value in .env counts as not provided.
collect_credentials() {
local key
for key in "$@"; do
if [[ -n ${CREDENTIALS[$key]:-} ]]; then
continue
fi
case "$key" in
GITHUB_USER)
prompt_value "$(credential_label "$key")" "" is_valid_github_owner \
"use letters, digits or '-' (at most 39)"
;;
*)
prompt_secret "$(credential_label "$key")" is_valid_token "$HINT_TOKEN"
SECRET_VALUES+=("$REPLY")
;;
esac
CREDENTIALS[$key]="$REPLY"
REPLY=""
done
}
+97
View File
@@ -0,0 +1,97 @@
# shellcheck shell=bash
# shellcheck disable=SC2004,SC2034,SC2154 # shared state and arrays are declared in constants.sh
# envfile.sh - The .env file of the new project: the one place a credential is written.
#
# Part of create-project.sh: sourced by it, never run on its own.
#
# Provides: env_file_keys, env_file_key_list, exclude_env_file, write_env_file, create_env_file
# env_file_keys: the credentials the new project needs, one per line: the
# Gitea token, and the GitHub token and account name when GitHub was chosen.
env_file_keys() {
printf '%s\n' GITEA_TOKEN
if ((PROJECT[has_github])); then
printf '%s\n' GITHUB_PAT GITHUB_USER
fi
}
# env_file_key_list: the same keys on one line, for messages.
env_file_key_list() {
local keys
keys="$(env_file_keys | tr '\n' ' ')"
printf '%s' "${keys% }"
}
# exclude_env_file DIR: make git ignore .env in DIR without touching a tracked
# file: the entry goes into .git/info/exclude, which is never committed. It
# does nothing when .env is already ignored.
exclude_env_file() {
local dir="$1" gitdir exclude
if git_project "$dir" check-ignore -q -- "$ENV_FILE_NAME"; then
return 0
fi
gitdir="$(git_project "$dir" rev-parse --absolute-git-dir)"
exclude="$gitdir/info/exclude"
mkdir -p -- "$gitdir/info"
# Start on a fresh line when the file does not end with one.
if [[ -s $exclude && -n "$(tail -c 1 -- "$exclude")" ]]; then
printf '\n' >>"$exclude"
fi
printf '%s\n' "# RepoFoundry: the credentials file of this project" "$ENV_FILE_NAME" >>"$exclude"
git_project "$dir" check-ignore -q -- "$ENV_FILE_NAME" ||
die "could not make git ignore $ENV_FILE_NAME in $dir; nothing was written to it"
}
# write_env_file DIR IS_REPLACE: write the credentials to DIR/.env. The file is
# created private (mode 600) from the start, never readable by others, even
# for a moment: it is written under umask 077 as a temporary file next to the
# target and moved into place. An existing file is only replaced when
# IS_REPLACE is 1, and a file that appears in the meantime is never replaced.
write_env_file() {
local dir="$1" is_replace="$2" target tmp key
target="$dir/$ENV_FILE_NAME"
tmp="$(umask 077 && mktemp "$dir/$ENV_FILE_NAME.XXXXXX")"
TEMP_FILES+=("$tmp")
{
while IFS= read -r key; do
printf '%s=%s\n' "$key" "${CREDENTIALS[$key]}"
done < <(env_file_keys)
} >"$tmp"
if ((is_replace)); then
mv -f -- "$tmp" "$target"
else
mv -n -- "$tmp" "$target"
if [[ -e $tmp ]]; then
die "$target appeared while it was being written; it was not replaced"
fi
fi
}
# create_env_file: the last step. Only after a yes (default no) is the .env
# written, and an existing one is only replaced after another yes. Nothing
# printed names a value, only the keys.
create_env_file() {
local label="Project .env" dir="${PROJECT[directory]}" keys is_replace=0
keys="$(env_file_key_list)"
begin_step "$label"
prompt_yes_no "Create a $ENV_FILE_NAME file in the project with the credentials it needs ($keys); only you can read it and git ignores it" n
if ! ((REPLY)); then
finish_step "$label" "skipped" "(you declined)"
return 0
fi
if git_project "$dir" ls-files --error-unmatch -- "$ENV_FILE_NAME" >/dev/null 2>&1; then
finish_step "$label" "skipped" "($ENV_FILE_NAME is tracked by git; it was not written)"
return 0
fi
if [[ -e $dir/$ENV_FILE_NAME || -L $dir/$ENV_FILE_NAME ]]; then
prompt_yes_no "$ENV_FILE_NAME already exists in the project. Replace it" n
if ! ((REPLY)); then
finish_step "$label" "kept" "(the existing $ENV_FILE_NAME was left as it was)"
return 0
fi
is_replace=1
fi
exclude_env_file "$dir"
write_env_file "$dir" "$is_replace"
finish_step "$label" "created" "($keys; only you can read it, git ignores it)"
}
+1
View File
@@ -54,4 +54,5 @@ print_plan() {
else
say "$(printf ' %-18s: %s' "Framework" "NOT possible without SSH to Gitea; you will be asked whether to go on without it")"
fi
say "$(printf ' %-18s: %s' "Project .env" "you are asked whether to create it ($(env_file_key_list))")"
}
+22 -1
View File
@@ -4,7 +4,7 @@
#
# Part of create-project.sh: sourced by it, never run on its own.
#
# Provides: prompt_value, prompt_choice, prompt_yes_no
# Provides: prompt_value, prompt_secret, prompt_choice, prompt_yes_no
# prompt_value LABEL DEFAULT VALIDATOR HINT: ask until VALIDATOR accepts the
# answer; the accepted answer is returned in REPLY.
@@ -27,6 +27,27 @@ prompt_value() {
done
}
# prompt_secret LABEL VALIDATOR HINT: like prompt_value for a secret. What is
# typed is not shown (read -s) and a refused answer is never repeated in the
# message. An empty answer is refused; there is no default.
prompt_secret() {
local label="$1" validator="$2" hint="$3" answer
while true; do
printf '%s (input is hidden): ' "$label" >&2
IFS= read -rs answer || {
printf '\n' >&2
die "no input available for '$label'"
}
printf '\n' >&2 # the newline that hidden input did not echo
answer="$(trim "$answer")"
if [[ -n $answer ]] && "$validator" "$answer"; then
REPLY="$answer"
return 0
fi
warn "invalid $label: $hint"
done
}
# prompt_choice LABEL DEFAULT CHOICE...: the answer is returned in REPLY.
prompt_choice() {
local label="$1" default="$2" answer
-13
View File
@@ -96,7 +96,6 @@ validate_credentials"
assert_status "$case_name" 1 "$STATUS"
assert_contains "$case_name message" "$ERR" "$expected"
done <<'EOF'
no Gitea token|GITHUB_USER=octo\n|GITEA_TOKEN is missing
token too short|GITEA_TOKEN=short\n|not a valid token
token with a backslash|GITEA_TOKEN=abc\\defgh12345\n|not a valid token
bad GitHub token|GITEA_TOKEN=abcdefgh12345\nGITHUB_PAT=bad token\n|GITHUB_PAT
@@ -104,18 +103,6 @@ bad GitHub user|GITEA_TOKEN=abcdefgh12345\nGITHUB_USER=-bad-\n|GITHUB_USER
EOF
}
test_github_credentials_required_only_when_chosen() {
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/e.env"
run_lib "" "parse_env_file \"$WORK/e.env\" CREDENTIAL_KEYS CREDENTIALS
validate_credentials
echo no-github-ok
ENV_FILE=\"$WORK/e.env\"
require_github_credentials"
assert_contains "Gitea-only .env is valid" "$OUT" "no-github-ok"
assert_status "GitHub credentials missing" 1 "$STATUS"
assert_contains "names the missing key" "$ERR" "GITHUB_PAT is missing"
}
test_validators() {
local fn value expected
while IFS='|' read -r fn value expected; do
+305
View File
@@ -0,0 +1,305 @@
#!/usr/bin/env bash
# test-credentials.sh - tests for the credentials that .env does not provide
# and for the .env file of the new project (MIL-005): they are asked without
# echo, the project .env is only written after a yes, owner-only and ignored
# by git, and no token appears anywhere else. Sourced by run-tests.sh.
# shellcheck disable=SC2016 # snippet and fixture text is literal on purpose
readonly NL=$'\n'
# credentials_input: the answers of a run with no .env at all and GitHub
# chosen: the Gitea token, the details, then the GitHub token and account.
credentials_input() {
printf '%s' "$FAKE_GITEA_TOKEN$NL$ANSWERS_GITHUB$FAKE_GITHUB_PAT${NL}octo-user$NL"
}
run_dry_cred() {
run_cli "$1" --config "$WORK/config.env" --env "$WORK/.env"
}
run_apply_cred() {
run_cli "$1" --apply --config "$WORK/config.env" --env "$WORK/.env"
}
# without_env: remove the .env of the fixtures.
without_env() {
rm -f -- "$WORK/.env"
}
# env_mode FILE: the permission bits, empty where the platform has none.
env_mode() {
case "$(uname -s 2>/dev/null || true)" in
MINGW* | MSYS* | CYGWIN*) ;;
*) stat -c '%a' -- "$1" 2>/dev/null || stat -f '%Lp' -- "$1" 2>/dev/null || true ;;
esac
}
# ------------------------------------------------------------ prompt_secret
test_prompt_secret_asks_again_and_never_repeats_the_answer() {
run_lib $'short\n\nlongenoughtoken1\n' \
'prompt_secret "Gitea access token" is_valid_token "needs 8 characters"; echo "[$REPLY]"'
assert_status "valid answer found" 0 "$STATUS"
assert_eq "valid answer returned" "[longenoughtoken1]" "$OUT"
assert_contains "told why" "$ERR" "invalid Gitea access token: needs 8 characters"
assert_not_contains "refused answer not repeated" "$ERR" "short"
assert_contains "says the input is hidden" "$ERR" "(input is hidden)"
}
test_prompt_secret_stops_when_input_ends() {
run_lib "" 'prompt_secret "Gitea access token" is_valid_token "x" </dev/null'
assert_status "end of input" 1 "$STATUS"
assert_contains "message names the credential" "$ERR" "no input available for 'Gitea access token'"
}
# ------------------------------------------------------ collect_credentials
test_collect_credentials_asks_only_what_is_missing() {
run_lib "$FAKE_GITHUB_PAT${NL}octo-user$NL" \
'CREDENTIALS[GITEA_TOKEN]=giteaFAKEtoken1234567890
collect_credentials GITEA_TOKEN GITHUB_PAT GITHUB_USER
printf "%s|%s\n" "${CREDENTIALS[GITHUB_USER]}" "${#SECRET_VALUES[@]}"'
assert_status "asked" 0 "$STATUS"
assert_eq "account name kept, one secret registered" "octo-user|1" "$OUT"
assert_contains "GitHub token asked" "$ERR" "GitHub personal access token"
assert_contains "account asked" "$ERR" "GitHub account name"
assert_not_contains "Gitea token not asked" "$ERR" "Gitea access token"
assert_not_contains "no token shown" "$ERR$OUT" "$FAKE_GITHUB_PAT"
}
test_a_token_that_is_asked_is_registered_as_a_secret() {
run_lib "$FAKE_GITEA_TOKEN$NL" \
'collect_credentials GITEA_TOKEN
warn "the token is giteaFAKEtoken1234567890 here"'
assert_status "asked" 0 "$STATUS"
assert_not_contains "redacted in later messages" "$ERR" "$FAKE_GITEA_TOKEN"
}
test_an_empty_value_in_env_counts_as_not_provided() {
printf 'GITEA_TOKEN=\nGITHUB_USER=\n' >"$WORK/e.env"
run_lib "$FAKE_GITEA_TOKEN$NL" \
'parse_env_file "'"$WORK"'/e.env" CREDENTIAL_KEYS CREDENTIALS
validate_credentials
collect_credentials GITEA_TOKEN
echo asked-ok'
assert_status "empty value tolerated" 0 "$STATUS"
assert_contains "asked instead" "$ERR" "Gitea access token"
}
test_validate_credentials_no_longer_requires_any() {
printf '# nothing\n' >"$WORK/e.env"
run_lib "" 'parse_env_file "'"$WORK"'/e.env" CREDENTIAL_KEYS CREDENTIALS
validate_credentials
echo fine'
assert_status "no credential required" 0 "$STATUS"
assert_eq "no error" "fine" "$OUT"
printf 'GITEA_TOKEN=short\n' >"$WORK/e.env"
run_lib "" 'parse_env_file "'"$WORK"'/e.env" CREDENTIAL_KEYS CREDENTIALS
validate_credentials'
assert_status "a provided bad token is still refused" 1 "$STATUS"
assert_contains "named" "$ERR" "GITEA_TOKEN"
}
# --------------------------------------------------------------- the run
test_env_is_optional_and_the_token_is_asked_without_echo() {
setup_hosts
without_env
run_dry_cred "$FAKE_GITEA_TOKEN$NL$ANSWERS_GITEA_ONLY"
assert_status "dry run without .env" 0 "$STATUS"
assert_contains "token asked" "$ERR" "Gitea access token (input is hidden)"
assert_contains "plan printed" "$OUT" "Plan:"
assert_not_contains "token not shown" "$OUT$ERR" "$FAKE_GITEA_TOKEN"
assert_not_contains "only reads" "$(calls)" "POST"
}
test_a_provided_credential_is_not_asked() {
setup_hosts
run_dry_cred "$ANSWERS_GITHUB"
assert_status "all provided" 0 "$STATUS"
assert_not_contains "no token prompt" "$ERR" "(input is hidden)"
assert_not_contains "no account prompt" "$ERR" "GitHub account name"
}
test_github_credentials_are_asked_only_when_github_is_chosen() {
setup_hosts
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env"
run_dry_cred "$ANSWERS_GITEA_ONLY"
assert_status "Gitea only" 0 "$STATUS"
assert_not_contains "no GitHub token asked" "$ERR" "GitHub personal access token"
assert_not_contains "no GitHub account asked" "$ERR" "GitHub account name"
run_dry_cred "$ANSWERS_GITHUB$FAKE_GITHUB_PAT${NL}octo-user$NL"
assert_status "GitHub chosen" 0 "$STATUS"
assert_contains "GitHub token asked" "$ERR" "GitHub personal access token (input is hidden)"
assert_contains "GitHub account asked" "$ERR" "GitHub account name"
assert_not_contains "token not shown" "$OUT$ERR" "$FAKE_GITHUB_PAT"
}
test_an_invalid_asked_value_is_asked_again_and_never_shown() {
setup_hosts
without_env
run_dry_cred "bad token${NL}$FAKE_GITEA_TOKEN$NL$ANSWERS_GITEA_ONLY"
assert_status "second answer accepted" 0 "$STATUS"
assert_contains "told it is invalid" "$ERR" "invalid Gitea access token"
assert_not_contains "refused value not shown" "$ERR$OUT" "bad token"
}
test_input_that_ends_stops_before_any_request() {
setup_hosts
without_env
run_dry_cred ""
assert_status "stopped" 1 "$STATUS"
assert_contains "key named" "$ERR" "no input available for 'Gitea access token'"
assert_eq "no request made" "" "$(calls)"
assert_not_contains "no creation report" "$OUT" "This is what exists now"
}
test_asked_tokens_do_not_leak_under_bash_x() {
setup_hosts
without_env
STATUS=0
PATH="$WORK/bin:$PATH" STUB_DIR="$WORK" TMPDIR="$WORK/tmp" "$BASH" -x "$SCRIPT" \
--config "$WORK/config.env" --env "$WORK/.env" <<<"$(credentials_input)" \
>"$WORK/out.txt" 2>"$WORK/err.txt" || STATUS=$?
assert_status "run under bash -x" 0 "$STATUS"
assert_not_contains "no Gitea token in the trace" "$(cat "$WORK/err.txt" "$WORK/out.txt")" "$FAKE_GITEA_TOKEN"
assert_not_contains "no GitHub token in the trace" "$(cat "$WORK/err.txt" "$WORK/out.txt")" "$FAKE_GITHUB_PAT"
}
# ---------------------------------------------------------- the project .env
test_the_dry_run_names_the_env_step_and_writes_nothing() {
setup_hosts
run_dry_cred "$ANSWERS_GITHUB"
assert_contains "plan line" "$OUT" "Project .env : you are asked whether to create it (GITEA_TOKEN GITHUB_PAT GITHUB_USER)"
assert_file_missing "no .env" "$WORK/my-app/.env"
}
test_the_project_env_is_not_created_without_a_yes() {
setup_hosts
run_apply_cred "${ANSWERS_GITHUB}y$NL$NL"
assert_status "run" 0 "$STATUS"
assert_file_missing "default is no" "$WORK/my-app/.env"
assert_contains "reported" "$OUT" "Project .env : skipped (you declined)"
setup_hosts
run_apply_cred "${ANSWERS_GITHUB}y${NL}n$NL"
assert_file_missing "explicit no" "$WORK/my-app/.env"
}
test_the_project_env_holds_only_the_needed_keys_and_is_private() {
setup_hosts
run_apply_cred "${ANSWERS_GITHUB}y${NL}y$NL"
assert_status "run" 0 "$STATUS"
assert_file_exists ".env created" "$WORK/my-app/.env"
assert_eq "exactly the needed keys" "GITEA_TOKEN=$FAKE_GITEA_TOKEN${NL}GITHUB_PAT=$FAKE_GITHUB_PAT${NL}GITHUB_USER=octo-user" "$(cat "$WORK/my-app/.env")"
assert_eq "owner-only" "$(env_mode "$WORK/my-app/.env")" "$([[ -z "$(env_mode "$WORK/my-app/.env")" ]] || echo 600)"
assert_contains "reported with the keys, not the values" "$OUT" "Project .env : created (GITEA_TOKEN GITHUB_PAT GITHUB_USER;"
assert_not_contains "no token in the output" "$OUT$ERR" "$FAKE_GITEA_TOKEN"
assert_not_contains "no GitHub token in the output" "$OUT$ERR" "$FAKE_GITHUB_PAT"
# Git ignores it without any tracked file changing.
check
if ! git -C "$WORK/my-app" check-ignore -q -- .env; then
fail ".env is not ignored by git"
fi
assert_not_contains "not listed by git status" "$(git -C "$WORK/my-app" status --porcelain)" ".env"
assert_contains "excluded locally" "$(cat "$WORK/my-app/.git/info/exclude")" ".env"
check
if [[ -e $WORK/my-app/.gitignore ]]; then
fail "a .gitignore was written; only .git/info/exclude may change"
fi
# No temporary file is left behind.
assert_eq "no leftover file" "" "$(find "$WORK/my-app" -maxdepth 1 -name '.env.*' -print)"
}
test_the_project_env_without_github_holds_only_the_gitea_token() {
setup_hosts
run_apply_cred "${ANSWERS_GITEA_ONLY}y${NL}y$NL"
assert_status "run" 0 "$STATUS"
assert_eq "one key" "GITEA_TOKEN=$FAKE_GITEA_TOKEN" "$(cat "$WORK/my-app/.env")"
assert_contains "reported" "$OUT" "Project .env : created (GITEA_TOKEN;"
}
test_asked_credentials_are_what_the_project_env_holds() {
setup_hosts
without_env
run_apply_cred "$(credentials_input)${NL}y${NL}y$NL"
assert_status "run" 0 "$STATUS"
assert_eq "the asked values" "GITEA_TOKEN=$FAKE_GITEA_TOKEN${NL}GITHUB_PAT=$FAKE_GITHUB_PAT${NL}GITHUB_USER=octo-user" "$(cat "$WORK/my-app/.env")"
assert_not_contains "no token in the output" "$OUT$ERR" "$FAKE_GITEA_TOKEN"
}
test_no_token_is_in_any_file_but_the_project_env() {
setup_hosts
without_env
run_apply_cred "$(credentials_input)${NL}y${NL}y$NL"
assert_status "run" 0 "$STATUS"
local hits
# The new project (with its .git folder), the script's temporary directory
# and its output; the stub curl's own request log is not part of the product.
hits="$(grep -rIl -F -e "$FAKE_GITEA_TOKEN" -e "$FAKE_GITHUB_PAT" "$WORK/my-app" "$WORK/tmp" "$WORK/out.txt" "$WORK/err.txt" 2>/dev/null |
grep -v -e '/my-app/\.env$' || true)"
assert_eq "only the project .env holds a token" "" "$hits"
}
test_an_existing_env_is_kept_unless_the_maintainer_says_replace() {
setup_hosts
mkdir -p "$WORK/my-app"
printf 'keep\n' >"$WORK/my-app/.env"
run_apply_cred "${ANSWERS_GITHUB}y${NL}y${NL}y${NL}n$NL"
assert_status "declined replacing" 0 "$STATUS"
assert_eq "unchanged" "keep" "$(cat "$WORK/my-app/.env")"
assert_contains "reported" "$OUT" "Project .env : kept (the existing .env was left as it was)"
remove_workdir
new_workdir
setup_hosts
mkdir -p "$WORK/my-app"
printf 'keep\n' >"$WORK/my-app/.env"
run_apply_cred "${ANSWERS_GITHUB}y${NL}y${NL}y${NL}y$NL"
assert_status "agreed to replace" 0 "$STATUS"
assert_contains "replaced" "$(cat "$WORK/my-app/.env")" "GITEA_TOKEN=$FAKE_GITEA_TOKEN"
assert_eq "private after replacing" "$(env_mode "$WORK/my-app/.env")" "$([[ -z "$(env_mode "$WORK/my-app/.env")" ]] || echo 600)"
}
test_a_tracked_env_is_never_written() {
mkdir -p "$WORK/p"
git -C "$WORK/p" init -q
printf 'tracked\n' >"$WORK/p/.env"
git -C "$WORK/p" add .env
git -C "$WORK/p" -c user.name=t -c user.email=t@example.test commit -q -m init
run_lib "y$NL" \
'PROJECT[directory]="'"$WORK"'/p"; PROJECT[has_github]=0
CREDENTIALS[GITEA_TOKEN]=giteaFAKEtoken1234567890
init_steps
create_env_file
echo "${STEP_STATUS["Project .env"]}"'
assert_status "run" 0 "$STATUS"
assert_eq "skipped" "skipped" "$OUT"
assert_eq "unchanged" "tracked" "$(cat "$WORK/p/.env")"
assert_contains "says why" "$(cat "$WORK/err.txt" "$WORK/out.txt")" "skipped"
}
test_exclude_is_added_once_and_keeps_the_existing_entries() {
mkdir -p "$WORK/p"
git -C "$WORK/p" init -q
printf 'build/' >"$WORK/p/.git/info/exclude" # no trailing newline
run_lib "" \
'exclude_env_file "'"$WORK"'/p"
exclude_env_file "'"$WORK"'/p"
echo done'
assert_status "run" 0 "$STATUS"
local exclude
exclude="$(cat "$WORK/p/.git/info/exclude")"
assert_contains "old entry kept" "$exclude" "build/"
assert_eq "entry added once" "1" "$(grep -c '^\.env$' "$WORK/p/.git/info/exclude")"
assert_eq "old entry still on its own line" "1" "$(grep -c '^build/$' "$WORK/p/.git/info/exclude")"
}
test_exclude_does_nothing_when_env_is_already_ignored() {
mkdir -p "$WORK/p"
git -C "$WORK/p" init -q
printf '.env\n' >"$WORK/p/.gitignore"
run_lib "" 'exclude_env_file "'"$WORK"'/p"; echo done'
assert_status "run" 0 "$STATUS"
assert_eq "exclude file untouched" "0" "$(grep -c '^\.env$' "$WORK/p/.git/info/exclude" || true)"
}
+3 -3
View File
@@ -47,12 +47,12 @@ test_full_run_without_github() {
assert_contains "plan says GitHub is not used" "$OUT" "GitHub repository : not used"
}
test_github_chosen_without_credentials_fails() {
test_github_chosen_without_credentials_stops_when_input_ends() {
write_fixtures
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env"
run_cli "$ANSWERS_GITHUB" --config "$WORK/config.env" --env "$WORK/.env"
assert_status "missing GitHub credentials" 1 "$STATUS"
assert_contains "names the key" "$ERR" "GITHUB_PAT is missing"
assert_status "missing GitHub credentials, no answer" 1 "$STATUS"
assert_contains "names the credential" "$ERR" "no input available for 'GitHub personal access token'"
assert_not_contains "no token in the error" "$OUT$ERR" "$FAKE_GITEA_TOKEN"
assert_eq "temporary files removed" "" "$(find "$WORK/tmp" -mindepth 1)"
}