honey_the_codewitch

submit_proj.ps1

Sep 1st, 2026
97
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. <#
  2. .SYNOPSIS
  3.     Bumps a project's own version across its release-metadata files (in sync,
  4.     leaving all dependency constraints untouched), then commits the changed
  5.     files, creates a lightweight git tag named exactly for that version, and
  6.     pushes the branch and tag.
  7.  
  8. .DESCRIPTION
  9.     Updates only the project's own version field in each file that is present:
  10.  
  11.         library.properties   version=<x>
  12.         idf_component.yml     top-level  version: "<x>"   (nested dep versions ignored)
  13.         library.json          top-level  "version": "<x>"
  14.         CMakeLists.txt        project(<name> ... VERSION <x>)   (any project name)
  15.  
  16.     Dependency versions (depends=..., GIT_TAG, FIND_PACKAGE_ARGS, nested
  17.     version: constraints, "pkg": "^1.2.3", etc.) are deliberately left alone.
  18.     Missing files are skipped; a file whose version field can't be found is
  19.     reported as a warning and left unchanged rather than guessed at.
  20.  
  21.     After a successful update it (unless -SkipGit):
  22.         1. stages EVERYTHING in the working tree (git add -A: new, modified,
  23.            and deleted files across the repo - not just the version files),
  24.         2. commits it all with the release message,
  25.         3. creates a lightweight tag whose name is the version verbatim
  26.            (optionally prefixed with -TagPrefix),
  27.         4. pushes the current branch and then the tag to -Remote.
  28.  
  29.     NOTE: because step 1 uses `git add -A`, anything currently in your working
  30.     tree goes into the release commit. Use .gitignore to keep out anything you
  31.     don't want committed (build output, local files, etc.).
  32.  
  33.     Git prerequisites (git present, folder is a work tree, remote exists, tag
  34.     not already present) are checked BEFORE any file is edited, so a bad setup
  35.     won't leave a half-modified tree.
  36.  
  37. .PARAMETER Version
  38.     The new version string, e.g. 2.0 or 1.4.3. Must start with a digit.
  39.     Positional #1.
  40.  
  41. .PARAMETER CommitMessage
  42.     The commit message, positional #2. A multi-word message must be quoted:
  43.         submit_proj 1.2.0 "this is a commit message"
  44.     If omitted, defaults to "Release <version>".
  45.  
  46. .PARAMETER Path
  47.     The project folder to operate on. Named only. Defaults to the current
  48.     directory.
  49.  
  50. .PARAMETER DryRun
  51.     Show what would change and which git commands would run, without writing
  52.     files or touching the repository.
  53.  
  54. .PARAMETER SkipGit
  55.     Update the files only; do not commit, tag, or push.
  56.  
  57. .PARAMETER Remote
  58.     Git remote to push to. Default: origin.
  59.  
  60. .PARAMETER TagPrefix
  61.     Optional prefix for the tag name (e.g. 'v' -> tag 'v2.0'). Default: none,
  62.     so the tag is the version exactly as given.
  63.  
  64. .EXAMPLE
  65.     .\submit_proj.ps1 1.2.0 "this is a commit message"
  66.     Bump present files to 1.2.0, commit everything with that message, tag '1.2.0', push.
  67.  
  68. .EXAMPLE
  69.     .\submit_proj.ps1 2.0
  70.     Same, using the default commit message "Release 2.0".
  71.  
  72. .EXAMPLE
  73.     .\submit_proj.ps1 1.5.0 "preview only" -DryRun
  74.     Preview the file edits and git commands without doing anything.
  75.  
  76. .EXAMPLE
  77.     .\submit_proj.ps1 1.6.0 -SkipGit
  78.     Only rewrite the version files; leave committing and tagging to you.
  79. #>
  80. #Requires -Version 5.1
  81. [CmdletBinding()]
  82. param(
  83.     [Parameter(Mandatory, Position = 0)]
  84.     [string]$Version,
  85.  
  86.     # Positional #2: the commit message. A normal string, so a multi-word
  87.     # message must be quoted:  submit_proj 1.2.0 "this is a commit message"
  88.     # Omit it to default to "Release <version>".
  89.     [Parameter(Position = 1)]
  90.     [string]$CommitMessage,
  91.  
  92.     # Optional flags below - used only when named, so they never interfere
  93.     # with the plain "submit_proj <version> <message>" call.
  94.     [string]$Path = (Get-Location).Path,
  95.     [switch]$DryRun,
  96.     [switch]$SkipGit,
  97.     [string]$Remote = 'origin',
  98.     [string]$TagPrefix = ''
  99. )
  100.  
  101. $ErrorActionPreference = 'Stop'
  102.  
  103. # Default the message when it wasn't supplied.
  104. if (-not $CommitMessage) { $CommitMessage = "Release $Version" }
  105.  
  106. # --- validate the version string -------------------------------------------
  107. if ($Version -notmatch '^[0-9][0-9A-Za-z.\-+]*$') {
  108.     Write-Error "Version '$Version' is invalid. It must start with a digit and use only 0-9 A-Z a-z . - +"
  109.     return
  110. }
  111. if ($Version -notmatch '^[0-9]+(\.[0-9]+){1,3}$') {
  112.     Write-Warning "Version '$Version' isn't plain numeric x.y[.z] - CMake and Arduino may reject it."
  113. }
  114.  
  115. if (-not (Test-Path -LiteralPath $Path -PathType Container)) {
  116.     Write-Error "Project folder not found: $Path"
  117.     return
  118. }
  119. $root = (Resolve-Path -LiteralPath $Path).Path
  120.  
  121. $tag = "$TagPrefix$Version"
  122. $doGit = -not $SkipGit
  123.  
  124. # --- git preflight (BEFORE editing any files) ------------------------------
  125. # In a real run, a failed check aborts before the tree is touched.
  126. # In -DryRun, checks only warn so the preview can still run.
  127. function Test-GitStep {
  128.     param([bool]$Ok, [string]$Message)
  129.     if ($Ok) { return $true }
  130.     if ($DryRun) { Write-Warning "$Message (git steps will be skipped in this preview)"; return $false }
  131.     Write-Error $Message
  132.     exit 1
  133. }
  134.  
  135. if ($doGit) {
  136.     if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
  137.         $doGit = Test-GitStep $false "git was not found on PATH."
  138.     }
  139. }
  140. if ($doGit) {
  141.     $inside = (& git -C $root rev-parse --is-inside-work-tree 2>$null)
  142.     $doGit = Test-GitStep (($LASTEXITCODE -eq 0) -and ($inside -eq 'true')) "'$root' is not inside a git working tree."
  143. }
  144. if ($doGit) {
  145.     & git -C $root remote get-url $Remote 1>$null 2>$null
  146.     $doGit = Test-GitStep ($LASTEXITCODE -eq 0) "Git remote '$Remote' is not configured in this repo."
  147. }
  148. if ($doGit) {
  149.     $existingTag = (& git -C $root tag --list $tag 2>$null)
  150.     # A duplicate tag is fatal in a real run; in a preview we only warn.
  151.     if ($existingTag) { $doGit = Test-GitStep $false "Tag '$tag' already exists in this repo." }
  152. }
  153.  
  154. # --- per-file replacement rules --------------------------------------------
  155. # Each regex has a named group 'v' = current project version (for reporting).
  156. # Anchoring guarantees only the project's OWN version can match - never a
  157. # dependency constraint. Nothing here keys off the project name.
  158. $rules = @(
  159.     @{
  160.         File  = 'library.properties'
  161.         # No $ anchor: it would choke on a Windows CRLF (\r) left before \n.
  162.         # \uFEFF? tolerates a byte-order mark; value runs to end of line.
  163.         Regex = [regex]'(?m)^(?<pre>\uFEFF?version[ \t]*=[ \t]*)(?<v>[^\r\n]*)'
  164.         Eval  = { param($m) $m.Groups['pre'].Value + $Version }
  165.     },
  166.     @{
  167.         File  = 'idf_component.yml'
  168.         # Column-0 'version:' only (nested dep versions are indented, so ignored).
  169.         # No $ anchor, for the same CRLF reason; the newline is left untouched.
  170.         Regex = [regex]'(?m)^(?<pre>\uFEFF?version:[ \t]*)(?<q>[''"]?)(?<v>[^''"\r\n]*)\k<q>'
  171.         Eval  = { param($m) $m.Groups['pre'].Value + $m.Groups['q'].Value + $Version + $m.Groups['q'].Value }
  172.     },
  173.     @{
  174.         File  = 'library.json'
  175.         Regex = [regex]'(?<pre>"version"[ \t]*:[ \t]*")(?<v>[^"]*)(?<post>")'
  176.         Eval  = { param($m) $m.Groups['pre'].Value + $Version + $m.Groups['post'].Value }
  177.     },
  178.     @{
  179.         File  = 'CMakeLists.txt'
  180.         Regex = [regex]'(?<pre>project\s*\([^)]*?\sVERSION\s+)(?<v>[0-9][0-9A-Za-z.\-+]*)'
  181.         Eval  = { param($m) $m.Groups['pre'].Value + $Version }
  182.     }
  183. )
  184.  
  185. # --- edit the files ---------------------------------------------------------
  186. $utf8NoBom = New-Object System.Text.UTF8Encoding($false)
  187. $results = @()
  188.  
  189. foreach ($rule in $rules) {
  190.     $file = Join-Path $root $rule.File
  191.  
  192.     if (-not (Test-Path -LiteralPath $file -PathType Leaf)) {
  193.         $results += [pscustomobject]@{ File = $rule.File; Status = 'missing'; Old = $null }
  194.         continue
  195.     }
  196.  
  197.     $content = Get-Content -LiteralPath $file -Raw
  198.     if ($null -eq $content) { $content = '' }
  199.  
  200.     $m = $rule.Regex.Match($content)
  201.     if (-not $m.Success) {
  202.         $results += [pscustomobject]@{ File = $rule.File; Status = 'no-match'; Old = $null }
  203.         continue
  204.     }
  205.  
  206.     $old = $m.Groups['v'].Value
  207.     $new = $rule.Regex.Replace($content, [System.Text.RegularExpressions.MatchEvaluator]$rule.Eval, 1)
  208.  
  209.     if ($new -eq $content) {
  210.         $results += [pscustomobject]@{ File = $rule.File; Status = 'unchanged'; Old = $old }
  211.         continue
  212.     }
  213.  
  214.     if (-not $DryRun) {
  215.         [System.IO.File]::WriteAllText($file, $new, $utf8NoBom)
  216.     }
  217.     $status = if ($DryRun) { 'would-update' } else { 'updated' }
  218.     $results += [pscustomobject]@{ File = $rule.File; Status = $status; Old = $old }
  219. }
  220.  
  221. # --- report file results ----------------------------------------------------
  222. Write-Host ''
  223. Write-Host ("Target version : {0}" -f $Version)
  224. Write-Host ("Project folder : {0}" -f $root)
  225. if ($DryRun)  { Write-Host 'Mode           : DRY RUN (nothing written or pushed)' -ForegroundColor Yellow }
  226. if ($SkipGit) { Write-Host 'Git            : skipped (-SkipGit)' -ForegroundColor DarkGray }
  227. Write-Host ''
  228.  
  229. foreach ($r in $results) {
  230.     switch ($r.Status) {
  231.         'updated'      { Write-Host ("  [updated]  {0,-20} {1} -> {2}" -f $r.File, $r.Old, $Version) -ForegroundColor Green }
  232.         'would-update' { Write-Host ("  [dry-run]  {0,-20} {1} -> {2}" -f $r.File, $r.Old, $Version) -ForegroundColor Cyan }
  233.         'unchanged'    { Write-Host ("  [ok]       {0,-20} already at {1}" -f $r.File, $Version) -ForegroundColor DarkGray }
  234.         'no-match'     { Write-Host ("  [warn]     {0,-20} version field not found - left unchanged" -f $r.File) -ForegroundColor Yellow }
  235.         # 'missing' is intentionally silent - a project legitimately may not ship every file.
  236.     }
  237. }
  238.  
  239. # Abort if this doesn't look like a project folder at all (none of the files present).
  240. $found = @($results | Where-Object { $_.Status -ne 'missing' })
  241. if ($found.Count -eq 0) {
  242.     Write-Host ''
  243.     Write-Host ("No version files found in {0}." -f $root) -ForegroundColor Red
  244.     Write-Host 'Expected at least one of: library.properties, idf_component.yml, library.json, CMakeLists.txt.' -ForegroundColor Red
  245.     Write-Host 'Nothing was changed and no tag was created - check you are in the right project folder.' -ForegroundColor Red
  246.     exit 1
  247. }
  248.  
  249. $changed  = @($results | Where-Object { $_.Status -in 'updated', 'would-update' })
  250. $warned   = @($results | Where-Object { $_.Status -eq 'no-match' })
  251. $filesToCommit = @($changed | ForEach-Object { $_.File })
  252.  
  253. Write-Host ''
  254. $verb = if ($DryRun) { 'to update' } else { 'updated' }
  255. Write-Host ("Files: {0} {1}, {2} warning(s)." -f $filesToCommit.Count, $verb, $warned.Count)
  256. if ($warned.Count -gt 0) {
  257.     Write-Host 'Review the [warn] file(s) - their version field did not match the expected layout.' -ForegroundColor Yellow
  258. }
  259.  
  260. # --- git: commit, tag, push -------------------------------------------------
  261. if (-not $doGit) { return }
  262.  
  263. Write-Host ''
  264. Write-Host 'Git:'
  265.  
  266. if ($DryRun) {
  267.     Write-Host ("  git -C `"{0}`" add -A" -f $root) -ForegroundColor Cyan
  268.     Write-Host ("  git -C `"{0}`" commit -m `"{1}`"   (if the tree has changes)" -f $root, $CommitMessage) -ForegroundColor Cyan
  269.     Write-Host ("  git -C `"{0}`" tag {1}" -f $root, $tag) -ForegroundColor Cyan
  270.     Write-Host ("  git -C `"{0}`" push {1} HEAD" -f $root, $Remote) -ForegroundColor Cyan
  271.     Write-Host ("  git -C `"{0}`" push {1} {2}" -f $root, $Remote, $tag) -ForegroundColor Cyan
  272.     return
  273. }
  274.  
  275. # stage and commit the ENTIRE working tree (new, modified, deleted)
  276. & git -C $root add -A
  277. if ($LASTEXITCODE -ne 0) { Write-Error 'git add -A failed.'; exit 1 }
  278.  
  279. & git -C $root diff --cached --quiet
  280. if ($LASTEXITCODE -eq 0) {
  281.     Write-Host '  working tree clean; nothing to commit, tagging current HEAD' -ForegroundColor DarkGray
  282. } else {
  283.     & git -C $root commit -m $CommitMessage
  284.     if ($LASTEXITCODE -ne 0) { Write-Error 'git commit failed.'; exit 1 }
  285.     Write-Host ("  committed all working-tree changes: {0}" -f $CommitMessage) -ForegroundColor Green
  286. }
  287.  
  288. # lightweight tag, name = version verbatim (+ optional prefix)
  289. & git -C $root tag $tag
  290. if ($LASTEXITCODE -ne 0) { Write-Error "git tag '$tag' failed."; exit 1 }
  291. Write-Host ("  created tag {0}" -f $tag) -ForegroundColor Green
  292.  
  293. # push branch, then tag
  294. & git -C $root push $Remote HEAD
  295. if ($LASTEXITCODE -ne 0) { Write-Error 'git push (branch) failed. The commit and tag exist locally but were not pushed.'; exit 1 }
  296.  
  297. & git -C $root push $Remote $tag
  298. if ($LASTEXITCODE -ne 0) { Write-Error "git push (tag '$tag') failed. The branch was pushed; the tag exists locally but was not pushed."; exit 1 }
  299.  
  300. Write-Host ("  pushed branch + tag '{0}' to {1}" -f $tag, $Remote) -ForegroundColor Green
  301. Write-Host ''
  302. Write-Host ("Release {0} complete." -f $Version) -ForegroundColor Green
Advertisement
Add Comment
Please, Sign In to add comment