License header year automation
Developer documentation for the automated license end-year update feature introduced to keep Krypton Suite source file headers aligned with the current calendar year.
Table of contents
- Overview
- Background: license headers in this repository
- Architecture
- Components
- Matching rules
- File scope
- Update algorithm
- Encoding and line endings
- GitHub Actions workflow
- Dry run mode
- Pull request behaviour
- Local development
- Maintaining headers in new and existing files
- What this automation does not update
- Operational runbook
- Troubleshooting
- Extending or modifying the automation
- Security and permissions
- FAQ
Overview
The Krypton Standard Toolkit repository carries BSD 3-Clause license headers on most source files. The modifications line records when ongoing Krypton Suite work began and the last year in which the file was maintained, using a range such as 2017 - 2026.
Each January (and on demand), maintainers previously had to update thousands of file headers manually. The Update License Year automation:
- Checks out the
alphabranch. - Scans the repository for eligible files.
- Updates the end year on lines containing
Modifications by Peter Wagnerwhen that year is behind the current calendar year. - Opens a pull request back into
alphafor review and merge.
The automation is intentionally conservative: it only changes the end year, never the start year, and only on lines that match the Krypton modifications attribution pattern.
Related files
| File | Purpose |
|---|---|
.github/workflows/update-license-year.yml |
GitHub Actions workflow definition |
.github/scripts/Update-LicenseYear.ps1 |
PowerShell implementation |
.github/update-license-year-pr.md |
Short PR description template for the initial feature PR |
Background: license headers in this repository
Krypton source files typically use a #region BSD License block at the top of the file. There are two historical attribution layers:
1. Original Component Factory attribution (unchanged)
Historical files retain the original Component Factory Pty Ltd notice, for example:
© Component Factory Pty Ltd, 2006 - 2016, (Version 4.5.0.0) All rights reserved.
This range is fixed and must not be updated by the automation.
2. Krypton Suite modifications attribution (updated)
Ongoing Krypton Suite work is attributed on a separate line, for example:
Modifications by Peter Wagner (aka Wagnerp), Simon Coghlan (aka Smurf-IV), Giduac & Ahmed Abdelhameed et al. 2017 - 2026. All rights reserved.
The automation targets only this line (and close variants). The start year (2017, 2021, 2024, 2026, etc.) reflects when the file was first created or first received Krypton modifications. The end year should reflect the current maintenance period.
Header variants in the wild
The repository has evolved over many years, so wording is not identical everywhere. All of the following are handled, provided the line contains Modifications by Peter Wagner and a four-digit year range:
| Variant | Example end-year format |
|---|---|
| Full multi-contributor block comment | ... et al. 2017 - 2026. All rights reserved. |
| Shorter TestForm-style header | ... et al. 2024 - 2026. All rights reserved. |
| YAML comment prefix | # Modifications by ... 2025 - 2026. All rights reserved. |
.editorconfig escaped template |
... et al. 2026 - 2026. All rights reserved. inside file_header_template |
Embedded seed text in .cs string literals |
... 2017 - 2026. All rights reserved. inside a @"..." block |
Separator spacing around the dash is preserved (- vs -).
Parallel header maintenance mechanisms
Headers are maintained through several complementary mechanisms:
| Mechanism | Location | Role |
|---|---|---|
Per-file #region BSD License blocks |
*.cs and related source |
Canonical in-file notice |
.licenseheader project templates |
e.g. Krypton.Toolkit.licenseheader |
IDE tooling for new files |
.editorconfig file_header_template |
Source/Current/.editorconfig, Source/Testing/.editorconfig |
Visual Studio / analyzer file-header insertion |
| Runtime interpolated strings | GlobalStaticValues.cs, KryptonCustomPaletteBase.cs |
UI seed text using DateTime.Now.Year |
The automation updates static text in the first three categories. Runtime-interpolated strings are already dynamic and are out of scope.
Architecture
flowchart TD
subgraph triggers [Triggers]
Cron["Schedule: 1 Jan 06:00 UTC"]
Manual["workflow_dispatch"]
DryRun["dry_run input (manual only)"]
end
subgraph runner [ubuntu-latest runner]
Checkout["checkout@v6\nref: alpha"]
Script["Update-LicenseYear.ps1\n-Year <current year>"]
Summary["GITHUB_STEP_SUMMARY\n(dry run only)"]
PR["create-pull-request@v7\n(if changes & not dry run)"]
NoOp["Log: no changes required"]
end
subgraph output [Outputs]
Branch["automation/license-year-YYYY"]
AlphaPR["PR → alpha"]
Logs["Job logs / summary"]
end
Cron --> Checkout
Manual --> Checkout
DryRun --> Script
Checkout --> Script
Script -->|dry run| Summary
Script -->|files changed, live run| PR
Script -->|no changes| NoOp
PR --> Branch
PR --> AlphaPR
Script --> Logs
Summary --> Logs
Design principles
- Branch from
alpha, merge toalpha— active development for the toolkit flows throughalpha; the automation never commits directly to protected branches. - Pull request, not direct push — even scheduled runs produce a reviewable PR rather than silent commits.
- Idempotent updates — re-running when headers are already current is a no-op.
- Minimal diff — only the end year changes; contributor names, punctuation, and separator spacing are preserved.
- Safe matching — unrelated copyright lines (Component Factory, third-party JDH Software, etc.) are excluded by the match predicate.
Components
.github/workflows/update-license-year.yml
GitHub Actions workflow named Update License Year.
Triggers:
| Trigger | dry_run |
Behaviour |
|---|---|---|
schedule (0 6 1 1 *) |
n/a (always live) | Updates files and opens PR if needed |
workflow_dispatch |
false (default) |
Updates files and opens PR if needed |
workflow_dispatch |
true |
Previews changes only; no writes, no PR |
Permissions:
contents: write— required to push the automation branch and commit changes viacreate-pull-request.pull-requests: write— required to open the PR.
Runner: ubuntu-latest with PowerShell 7 (pwsh).
.github/scripts/Update-LicenseYear.ps1
Standalone PowerShell 7 script usable locally or in CI.
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
-Year |
int |
(Get-Date).Year |
Target end year |
-DryRun |
switch |
off | List affected files without writing |
Exit output: writes the count of affected files to stdout (captured by the workflow as updated_files).
Repository root resolution:
- Uses
$env:GITHUB_WORKSPACEwhen running in Actions. - Falls back to
(Get-Location).Pathfor local runs.
Matching rules
Regular expression
'(Modifications by Peter Wagner[^\r\n]*?)(\d{4})(\s*-\s*)(\d{4})'
| Capture group | Content | Example |
|---|---|---|
| 1 | Prefix through start of year range | Modifications by Peter Wagner ... et al. |
| 2 | Start year | 2017 |
| 3 | Separator (spaces + dash + spaces) | - |
| 4 | End year | 2025 |
Replacement logic
For each match on a given line:
if endYear >= targetYear:
leave unchanged
else:
replace endYear with targetYear
Examples
| Input range | Target year | Output range | Notes |
|---|---|---|---|
2025 - 2025 |
2026 | 2025 - 2026 |
Common for files created in the prior year |
2017 - 2025 |
2026 | 2017 - 2026 |
Bulk of legacy toolkit files |
2017 - 2026 |
2026 | 2017 - 2026 |
Already current |
2026 - 2026 |
2027 | 2026 - 2027 |
Next calendar year rollover |
2006 - 2016 |
any | unchanged | No Modifications by Peter Wagner on line |
2013-2021 (JDH) |
any | unchanged | Third-party header; different pattern |
Multiple matches per file
If a file contains more than one matching line (for example, a header block and an embedded string literal with the same attribution), all matching lines in that file are updated in a single pass.
File scope
Included extensions
| Extension / name | Typical use |
|---|---|
.cs |
C# source, designers, global usings |
.licenseheader |
Project license header templates |
.yml, .yaml |
GitHub workflows, issue templates |
.md |
Documentation with header comments |
.editorconfig |
file_header_template year range |
Excluded paths
Files under these directory segments are skipped:
bin/obj/Artefacts/.git/
Not scanned
The following are intentionally not in scope today:
| Pattern | Reason |
|---|---|
*.csproj, *.sln, *.resx |
No standard modifications header |
LICENSE (repo root) |
Uses Copyright (c) YYYY - YYYY without the modifications line |
Documents/License/License.md |
Same as above; update manually if required |
| Binary and generated artefacts | Excluded by path rules |
If new file types adopt the modifications header pattern, add their extension to the $extensions set in Update-LicenseYear.ps1.
Update algorithm
High-level steps executed by Update-LicenseYear.ps1:
- Resolve repository root.
- Recursively enumerate all files under the root.
- For each file:
- Skip if path matches an excluded segment.
- Skip if extension is not in the allow-list (unless the file is named
.editorconfig). - Read raw bytes from disk.
- Detect UTF-8 BOM presence and decode content accordingly.
- Apply regex replacement across the full file content.
- If content unchanged, continue to next file.
- If
-DryRun, logWould update: <relative-path>and continue without writing. - Otherwise, encode and write bytes back, preserving BOM state.
- Emit summary and return the count of affected files.
The script processes the entire repository tree under the checkout root, including Source/Current, Source/Testing, Scripts/, .github/, and Documents/ when matching files exist there.
Encoding and line endings
The Krypton repository standard (see AGENTS.md and Source/*/.editorconfig) is UTF-8 with BOM and CRLF line endings for C# source.
The script is encoding-aware:
- BOM detection — if the file begins with
EF BB BF, it is treated as UTF-8 with BOM and the BOM is preserved on write. - No BOM — files without a BOM are written back without adding one.
- Line endings — content is decoded and re-encoded as a single string; existing
\r\nor\nsequences in the file are not normalised. The automation does not force CRLF conversion.
This avoids the common pitfall of Linux CI runners rewriting entire trees to LF or stripping BOMs from WinForms designer files.
GitHub Actions workflow
Step-by-step execution
1. Checkout alpha
uses: actions/checkout@v6
with:
ref: alpha
fetch-depth: 0
The workflow always works from alpha, regardless of which branch contains the workflow definition.
2. Update license header end year
- Determines
$yearfrom(Get-Date).Yearon the runner (UTC date onubuntu-latest). - Parses
dry_runfromworkflow_dispatchinputs; scheduled runs always perform a live update. - Invokes
Update-LicenseYear.ps1. - Records
year,dry_run, andupdated_filesas step outputs. - In dry-run mode, writes a markdown summary to
$GITHUB_STEP_SUMMARY.
3. Create pull request into alpha
Runs only when:
dry_runis nottrue, andupdated_filesis not0.
Uses peter-evans/create-pull-request@v7 with:
| Setting | Value |
|---|---|
base |
alpha |
branch |
automation/license-year-YYYY |
delete-branch |
true (branch removed after merge) |
If a PR from the same branch already exists, the action updates it with new commits.
4. No changes required
Logs an informational message when updated_files is 0, distinguishing dry-run vs live wording.
Dry run mode
Dry run is available only via manual workflow_dispatch. Scheduled January runs always apply changes.
GitHub UI
- Open Actions → Update License Year.
- Click Run workflow.
- Enable Preview changes without modifying files or creating a PR.
- Run.
What to expect
| Artifact | Content |
|---|---|
| Job log | One Would update: <path> line per affected file |
| Job summary | Target year and total file count |
| Repository | Unchanged — no branch, commit, or PR |
When to use dry run
- After merging workflow changes, to validate matching rules before the first live run.
- Before 1 January, to estimate PR size for the upcoming annual update.
- After modifying the regex or extension list, to verify scope.
Pull request behaviour
Branch naming
automation/license-year-2026
The year suffix matches the target end year applied by the script.
PR title and commit message
chore: update license header end year to 2026
Update license header end year to 2026
PR body (automated)
The generated PR body includes:
- Target year
- Number of files updated
- Note that it was triggered by the Update License Year workflow
Review guidance
Automated license PRs are mechanically simple but can be large. Recommended review approach:
- Confirm the diff only touches year digits on
Modifications by Peter Wagnerlines. - Spot-check
.licenseheaderand.editorconfigtemplates — these affect new files created by IDE tooling. - Verify no unexpected paths appear (if they do, refine exclusions or the regex).
- Merge into
alphabefore the next development sprint if the run was triggered on 1 January.
Re-running after merge
Once all headers show the current end year, subsequent runs (dry or live) report zero changes and do not open a new PR.
Local development
Prerequisites
- PowerShell 7+ (
pwsh)
Preview changes (recommended)
From the repository root:
pwsh -File .github/scripts/Update-LicenseYear.ps1 -Year 2026 -DryRun
Apply changes locally
pwsh -File .github/scripts/Update-LicenseYear.ps1 -Year 2026
Review with git diff before committing. Local application is useful when testing regex changes; production updates should normally flow through the GitHub workflow and PR process.
Target a specific year
pwsh -File .github/scripts/Update-LicenseYear.ps1 -Year 2027 -DryRun
Useful when validating next-year rollover logic before 31 December.
Maintaining headers in new and existing files
Adding a new C# file
Use the IDE file-header command (documented in
.editorconfig):Visual Studio:
Ctrl+.on first line → Add file header, or Fix all in Solution.Or copy the
#region BSD Licenseblock from a sibling file in the same project.Set the start year to the file creation year. The end year should be the current calendar year.
Ensure the modifications line includes
Modifications by Peter Wagnerso future automation picks it up.
Updating project templates
When the annual PR merges, verify these templates were included:
Source/*/Krypton Components/*/*.licenseheaderSource/Current/.editorconfig→file_header_templateSource/Testing/.editorconfig→file_header_template
If template files lag behind, new files created via IDE tooling may receive an outdated end year until the next automation run.
Manual mid-year updates
Developers may set the end year to the current year when making substantial edits to a file. This is optional but keeps headers accurate between annual runs. The automation will skip files already at or beyond the target year.
What this automation does not update
| Item | Why |
|---|---|
Component Factory 2006 - 2016 lines |
Different attribution; historically fixed |
JDH Software Copyright (C) 2013-2021 |
Third-party code; different format |
GlobalStaticValues.DEFAULT_*_SEED_TEXT |
Uses {DateTime.Now.Year} at runtime |
KryptonCustomPaletteBase export strings |
Uses {DateTime.Now.Year} at runtime |
Root LICENSE and Documents/License/License.md |
Copyright (c) format without modifications line |
ViewLayoutMonths.cs DateTime.Now.Year |
Unrelated calendar UI logic |
If the root LICENSE or License.md copyright range must track the calendar year, that remains a manual maintainer task unless the regex scope is deliberately extended.
Operational runbook
Annual process (automatic)
| When | What happens |
|---|---|
| 1 Jan, 06:00 UTC | Scheduled workflow runs on alpha |
| After workflow completes | Review PR automation/license-year-YYYY |
| Within first week of year | Merge PR into alpha |
Annual process (manual fallback)
If the scheduled run fails or is disabled:
- Run Update License Year manually (
dry_run: false). - Review and merge the resulting PR.
Pre-year checklist (optional, December)
- Run dry run on
alpha. - Note file count in team channel.
- Confirm
.editorconfigfile_header_templateend year in active branches (automation fixes this in the PR).
Post-merge checklist
- [ ] Automation PR merged into
alpha - [ ] Dry run reports
0files would be updated - [ ] New file header insertion in Visual Studio shows current end year
Troubleshooting
Workflow did not open a PR
| Symptom | Likely cause | Action |
|---|---|---|
| Job succeeded, no PR | All headers already current | Expected; check log for updated_files=0 |
| Job succeeded, no PR | Dry run was enabled | Re-run with dry run disabled |
| Step skipped | updated_files == 0 |
Run locally with -DryRun to confirm |
PR contains unexpected files
| Symptom | Likely cause | Action |
|---|---|---|
| Workflow YAML files changed | YAML headers use # Modifications by... |
Expected if end year was behind |
.md files changed |
Header comment blocks in docs | Expected if matched |
| Build artefact paths | Should not occur | Report bug; verify bin//obj/ exclusions |
File not updated but should have been
- Confirm the line contains the exact substring
Modifications by Peter Wagner. - Confirm the file extension is in the allow-list.
- Confirm the end year is strictly less than the target year.
- Confirm the file is not under an excluded path.
File updated but should not have been
- Check whether the line incorrectly contains
Modifications by Peter Wagnerin embedded documentation or sample text. - If a false positive is found, refine the regex or add a path exclusion and document the exception here.
PowerShell errors on runner
The script requires PowerShell 7 (#requires -Version 7.0). ubuntu-latest GitHub-hosted runners include pwsh. Self-hosted runners must install PowerShell 7.
create-pull-request permission errors
Ensure workflow permissions grant contents: write and pull-requests: write. Organisation policies that restrict GITHUB_TOKEN may block branch push; consult repository admin settings.
Extending or modifying the automation
Add a new file extension
Edit Update-LicenseYear.ps1:
$extensions = [System.Collections.Generic.HashSet[string]]::new(
[string[]]@('.cs', '.licenseheader', '.yml', '.yaml', '.md', '.your-ext'),
[StringComparer]::OrdinalIgnoreCase
)
Run -DryRun and review scope before merging.
Change the target branch
Update both checkout.ref and create-pull-request.base in update-license-year.yml. Branch naming and documentation should stay aligned.
Change the schedule
Cron expression in update-license-year.yml:
- cron: '0 6 1 1 *' # minute hour day month weekday (UTC)
Tighten the match predicate
To require All rights reserved after the year range, extend the regex cautiously and re-run dry run across the full tree — some embedded strings may use different trailing text.
Add a kill switch
Other workflows in this repository (for example alpha-backup-sync.yml) use repository variables as kill switches. The same pattern can be added if maintainers want to disable scheduled runs without removing the workflow file.
Security and permissions
| Topic | Detail |
|---|---|
| Token | Default GITHUB_TOKEN scoped to the repository |
| Branch access | Writes only to automation/license-year-* branches, not directly to alpha |
| Fork PRs | Workflow does not run on fork pull requests; it is schedule/dispatch only |
| Script injection | Year is derived from the runner clock, not user input; dry_run is a boolean GitHub input |
| Review | All changes pass through a normal pull request review process |
FAQ
Why alpha and not master?
Active toolkit development for the current major line flows through alpha. The automation targets the branch where day-to-day header drift occurs.
Why only the end year?
The start year records when Krypton modifications to that file began. Changing it would misrepresent file history. The end year communicates ongoing maintenance.
Will the January run commit directly to alpha?
No. It always opens a PR. Maintainers merge after review.
Can I test the workflow before 1 January?
Yes. Use workflow_dispatch with dry run enabled, or run the PowerShell script locally with -DryRun.
Does the script run on Windows agents?
The script is cross-platform. The workflow uses ubuntu-latest for consistency with other repository workflows.
What if two maintainers run the live workflow simultaneously?
create-pull-request updates an existing open PR on the same branch name rather than creating duplicates.
Should I update headers in my feature PR?
Optional for the end year if your PR merges before the annual automation PR. Required: include a correctly formatted header on new files, with an appropriate start year and a current end year.
See also
- BSD 3-Clause License
- Repository guidelines (
AGENTS.md) — encoding, line endings, and header conventions