Project

General

Profile

Git Workflow » History » Revision 2

Revision 1 (Redmine Admin, 10/25/2026 04:16 PM) → Revision 2/11 (Redmine Admin, 10/26/2026 02:24 AM)

# Git Workflow (`/srv/git`, read-only into Redmine) 

 Repos are bare, owned by the restricted host user `git` (git-shell) — the one 
   

 deliberate exception to the no-host-packages rule. Redmine only *reads* them. 

 

 **Who does what — this is the part that causes confusion, so read it first:** 

 

 | Step                                     | Run as            | Why                                                          | 
 |----------------------------------------|-----------------|------------------------------------------------------------| 
 
 | --- | --- | --- | 
 | create bare repo, ACL, SELinux label     | **root**          | `/srv/git` is root/git territory                             | 
 
 | `git add` / `git commit` in a working copy | **owner user** (websvcmaster) | keeps `.git/objects` owned by that user            | 
 
 | commit + push, day to day                | **root**, via `deploy-commit.sh` | it commits *as websvcmaster* and pushes *as root* itself — the only command you normally type | 
 
 | `git push` by hand                       | **root**          | push needs `sudo -u git git-receive-pack`; websvcmaster has **no sudo** and never will | 
 
 | register repo in Redmine                 | Redmine admin UI |                                                                | 

 

 websvcmaster's password is **locked** on purpose (`passwd -l`, set by 
   

 `01-bootstrap-root.sh`). Enter it only via `su - websvcmaster` from root — never 
   

 set a password for it. If a command run as websvcmaster asks for a password, it 
   

 is a root command run in the wrong shell: `exit` to root and run it there. 
   

 (`passwd -S websvcmaster` must show `L`.) 

 

 Repos: `mds-dark.git` (WordPress theme, project "MDS / MIMMAR Web Page Design"), 
   

 `dmz-web-deploy.git` (this host's `~/deploy`, project "dmz-web Infrastructure"). 

 

 ### How Redmine reads them 

 ```ini 
 

 ``` 
 # site-redmine.container 
 
 Volume=/srv/git:/srv/git:ro 
 
 Environment=GIT_CONFIG_COUNT=1 
 
 Environment=GIT_CONFIG_KEY_0=safe.directory 
 
 Environment=GIT_CONFIG_VALUE_0=* 
 
 ``` 

 

 Rootless UID mapping: container uid *n* → host uid **200000 + n − 1**; Redmine 
   

 runs as uid 999 → host **200998** (`podman top site-redmine user huser`). 
   

 One-time host prep (root): 

 ```bash 
 

 ``` 
 semanage fcontext -a -t container_file_t '/srv/git(/.*)?' 
 
 restorecon -Rv /srv/git 
 
 ``` 

 

 ### A. New bare repo — root 

 ```bash 
 

 ``` 
 R=/srv/git/<repo>.git 
 
 sudo -u git git init --bare $R 
 
 sudo -u git git -C $R symbolic-ref HEAD refs/heads/main     # bare HEAD defaults to master; every repo here uses main 
 
 setfacl -R    -m u:200998:rX $R                               # Redmine read access 
 
 setfacl -R -d -m u:200998:rX $R                             # ...also for files future pushes create 
 
 restorecon -Rv $R        # "not reset as customized by admin" on every file = already container_file_t = OK 
 
 git config --global --add safe.directory /home/<user>/<workdir>     # lets root push from that working copy 
 
 ``` 

 

 ### B. Working copy, first time — owner user (`su - <user>` from root) 

 ```bash 
 

 ``` 
 git config --global user.name    "<user>"                     # once per user, or commit refuses 
 
 git config --global user.email "sysadmin@mdsdefense.com"    # matching a Redmine user's email links commits to them 
 
 cd <workdir> 
 
 git init -b main 
 
 # .gitignore first: no *.env, *secret*, *password*, *.key/*.pem/*.crt, dumps, archives, data/, tmp/ 
 
 git add -A && git status --short 
 
 grep -nEi 'password|secret|token|key_base' -r . --exclude-dir=.git | grep -viE 'Secret=|podman secret|\$\{?[A-Z_]*(PASSWORD|SECRET)'     # must be comments/prompts only 
 
 git commit -m "initial import $(date +%F)" 
 
 git remote add origin /srv/git/<repo>.git 
 
 git log --oneline 
 
 exit                                                        # back to root 
 
 ``` 

 

 ### C. Push — root 

 

 First push of a new working copy (once): 

 ```bash 
 

 ``` 
 cd /home/<user>/<workdir> 
 
 git push --receive-pack='sudo -u git git-receive-pack' -u origin main 
 
 chown -R <user>:<user> .git            # root's push leaves root-owned files in .git 
 
 ``` 

 

 Expected: `* [new branch] main -> main`. Verify: `sudo -u git git -C /srv/git/<repo>.git log --oneline`. 
   

 After that, never push by hand — use `deploy-commit.sh` (E). 

 

 ### D. Redmine 

 

 Project → Settings → Repositories → New repository: SCM **Git**, Main repository 
   

 ✓, Identifier `<repo>`, Path `/srv/git/<repo>.git`, Report last commit ✓ → Create. 
   

 Open the Repository tab; the commit must be listed. If not: 
   

 `podman exec site-redmine git -C /srv/git/<repo>.git log --oneline` — 
   

 "dubious ownership" → safe.directory env missing; "permission denied" → ACL (step A). 

 

 ### E. Day-to-day — `deploy-commit.sh` (root) 

 

 One root command does the whole cycle in the right identities: `git add -A` 
   

 and `git commit` **as websvcmaster** (identity auto-set), a guard that refuses 
   

 to commit staged lines that look like literal credentials, `git push` 
   

 **as root** via `sudo -u git git-receive-pack`, optional annotated tag, and 
   

 `chown` of `.git` back to websvcmaster. Source: `root-bin/deploy-commit.sh` in 
   

 the repo; installed as `/usr/local/sbin/deploy-commit.sh` (0750). 

 ```bash 
 

 ``` 
 deploy-commit.sh -n                           # dry run: shows what is staged, commits nothing 
 
 deploy-commit.sh -m "what changed"            # commit + push 
 
 deploy-commit.sh -m "cheat sheet v1.1" -t cheatsheet-1.1     # ...+ tag 
 
 deploy-commit.sh -t cheatsheet-1.1            # nothing to commit: tag HEAD, push the tag 
 
 deploy-commit.sh -y -m "..."                  # skip the credential guard after checking the flagged lines 
 
 deploy-commit.sh -h 
 
 ``` 

 

 Editing still happens as websvcmaster (`su - websvcmaster`, edit, `exit`); 
   

 `deploy-commit.sh` never edits files, only `.git`. The guard flags staged lines of 
   

 the form `<…password|secret|token|api_key|app_key…> = <literal of 8+ chars>`; 
   

 variables (`$X`), placeholders (`<…>`, `...`), Quadlet `Secret=` lines and 
   

 comments are ignored. If it fires: move the value to a Podman secret or 
   

 `.gitignore` the file and re-run; if the lines are harmless, re-run with `-y`. Install/update the script: 

 ```bash 
 

 ``` 
 install -o root -g root -m 0750 /home/websvcmaster/deploy/root-bin/deploy-commit.sh /usr/local/sbin/deploy-commit.sh 
 
 ``` 

 

 ### F. Publishing finished work — `deploy-publish.sh` (root) 

 Replaces the manual "copy from `/root` into the repo, fix owner, commit, tag, type the wiki into 
 Redmine" cycle. A delivery is **one file**, `dmz-web-release-<version>.zip` (`<version>` = the cheat sheet 
 version of that release), dropped into `/root`; run `deploy-publish.sh` as root. It calls 
 `deploy-commit.sh` for the git part, so E still applies. 

 The package holds the changed files (scripts, the cheat sheet), a `wiki/` folder (pages + png), 
 `RELEASE` (version, build date) and `SHA256SUMS`. What the script does: 

 0. **Package:** unpacked into a fresh private directory; paths are checked (no `..`, absolute paths 
    or symlinks), the file name must match `RELEASE`, `SHA256SUMS` must verify, and a file that is not 
    listed in `SHA256SUMS` rejects the package. 
 1. **Repo:** each known file is compared by its **version header** with the repo / installed copy: 
    newer → installed (repo files owned by websvcmaster, `/usr/local/sbin` copies root 0750); 
    identical → nothing to do; **older, same version with different content, or no version header → 
    refused** (stops a stale copy overwriting a newer one; `-f` overrides). Known files: cheat sheet, 
    `snipeit-install.sh`, `add-wordpress-site.sh`, `redmine-install.sh`, `check-sites.sh`, 
    `verify-host-deps.sh`, `deploy-commit.sh`, `deploy-publish.sh` — add a line to the `ITEMS` table in 
    the script for another one. 
 2. **Commit:** `deploy-commit.sh -m "release <version>: <files and versions>"`; a cheat sheet install also 
    gets the tag `cheatsheet-<version>` (skipped if that tag exists). 
 3. **Wiki:** `wiki/*.md` → Redmine REST API: page created or updated (parent `Wiki`, the duplicate 
    `# title` first line dropped, `.png` attached once, unchanged pages skipped). `@@COMMIT@@` in a page 
    (the Releases table) becomes the commit made in step 2 (on a re-run: the commit of the tag `cheatsheet-<version>`; a page that needs a hash and has none is skipped, never overwritten with the placeholder). 
 4. **Tidy:** after a complete, clean run the package is **moved** (never deleted) to 
    `/root/deploy-done/<timestamp>/`. With refusals, errors, `-n`, `-R` or `-W` it stays in `/root`; running it 
    again is safe, handled files then show as *identical* and are skipped. 

 Loose files (no package) still work: put them into `/root` (and `dmz-web-wiki-pages.zip` for the wiki) 
 and the handled ones are moved to `/root/deploy-done/<timestamp>/`. 

 ```bash 
 deploy-publish.sh -n            # dry run: the plan, wiki read-only GETs, nothing changed or moved 
 deploy-publish.sh               # do everything with the newest /root/dmz-web-release-*.zip 
 deploy-publish.sh -R            # wiki only              deploy-publish.sh -W     # repo only 
 deploy-publish.sh -m "msg"      # own commit message     deploy-publish.sh -y     # skip deploy-commit's guard 
 deploy-publish.sh -f            # install even when older / equal / no version header 
 ``` 

 One-time setup for the wiki step: `deploy-publish.sh --init` writes `/root/.deploy-publish.conf` 
 (mode 0600; set `RM_PROJECT` to the project **identifier**, Project settings → Identifier); put the 
 Redmine API key (My account → API access key) into `/root/.redmine-api-key`, `chmod 600`; Redmine 
 Administration → Settings → API → *Enable REST web service*. Requests go to NPM on this host 
 (`--resolve track.mdsdefense.com:444:127.0.0.1`), like `check-sites.sh`. The page text in Redmine is 
 overwritten by the package's text — the first real run replaces pages that were typed in by hand, so 
 start with `-n`. Bootstrap the first time (the script cannot install itself before it exists): 

 ```bash 
 install -o root -g root -m 0750 /root/deploy-publish.sh /usr/local/sbin/deploy-publish.sh 
 deploy-publish.sh --init && deploy-publish.sh -n 
 ``` 

 Afterwards the script updates itself from the package (the running copy is replaced safely). 

 ### Recovering from the usual mistakes 

 * 

 - `fatal: could not open '.git/COMMIT_EDITMSG': Permission denied` (or any 
     

 permission error on commit) → root touched `.git` (committed there, or pushed/tagged 
     

 without `deploy-commit.sh`). Fix: `chown -R <user>:<user> /home/<user>/<workdir>/.git`, 
     

 then use `deploy-commit.sh` from now on (it chowns every time). Also check root's `~/.gitconfig` didn't pick up the user's identity. 
 * 
 - `Everything up-to-date` on push right after you expected a new commit → the 
     

 commit didn't happen (identity error or permission error scrolled past); `git log --oneline` 
     

 and `git status --short` in the working copy. A tag made at that moment points at the 
     

 old commit: `deploy-commit.sh -t <tag>` after the real commit (it force-moves the tag). 
 * 
 - `error: src refspec main does not match any` → no commit yet on `main` (step B not done). 
 * 
 - `error: key does not contain a section: receivepack` → wrong syntax; use 
     
 `--receive-pack=` (step C), not `-c receivepack=`. 
 * 
 - `sudo: a password is required` / password prompt → you are websvcmaster; this is a root step (`exit`, then `deploy-commit.sh`). 
 * 
 - Redmine shows the repo empty → bare HEAD still points to `master` (step A `symbolic-ref`). 
 * `deploy-publish.sh`: `REFUSED: no version header` → the copy is an old/unversioned one; fetch the current file or `-f` if you are sure. `checksum mismatch` / `does not match RELEASE` → the package is damaged or was renamed; get it again. `HTTP 401/403` on the wiki step → API key wrong, REST not enabled, or the account may not edit this project's wiki. `HTTP 404` for every page → wrong `RM_PROJECT` (it must be the identifier, not the project name). 

 
 --- 
 _Source: 

 *Source: `dmz-web-podman-cheatsheet.md` v1.4.1 v1.1.1 in the `dmz-web-deploy` repo ([[Releases]]). ([Releases](#)). Edit the wiki and the repo copy together._ 
 together.* 
 Git Workflow (/srv/git, read-only into Redmine) 
 [How Redmine reads them](#)[A. New bare repo — root](#)[B. Working copy, first time — owner user (su - <user> from root)](#)[C. Push — root](#)[D. Redmine](#)[E. Day-to-day — deploy-commit.sh (root)](#)[Recovering from the usual mistakes](#) 
 ☰