-
Notifications
You must be signed in to change notification settings - Fork 11
feat: add semantic versioning, CI release automation and the override model #31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ldastey-dev
wants to merge
20
commits into
main
Choose a base branch
from
feat/versioning-and-override-system
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
a1f1f81
refactor(scripts): move deploy scripts and tests into scripts/
ldastey-dev c73e9a5
feat(overrides): add immutable base with consumer-owned override layer
ldastey-dev 2a965a7
feat(update): add update and migrate tooling for deployed repos
invalid-email-address 8c0eca0
ci(release): add version gate, PR title check and release automation
invalid-email-address 6bcd4cb
docs(versioning): document the override model, versioning and update …
invalid-email-address 641fb44
fix(ci): allow bootstrap of CI-owned files and restore executable bits
invalid-email-address ec6e6a0
fix(release): publish 1.0.0 as the initial drop instead of bumping pa…
invalid-email-address 7a09baf
docs(readme): explain what each version bump means for a deployment
invalid-email-address f41aa5a
chore: ignore Node heap dump reports
invalid-email-address 732fa7f
fix(content): repair broken index routes and separate migration baseline
ldastey-dev 8317ba4
docs(readme): correct baseline ownership claim
ldastey-dev 4124f11
fix(update,ci): close data-loss and version-derivation defects from r…
ldastey-dev 189368b
fix(powershell): restore parity with the bash migrate and update fixes
ldastey-dev cad8b6e
fix(deploy,update): protect the override layer and fix cross-platform…
ldastey-dev 60dff5d
fix(deploy): apply the overwrite guard to the shipped update tooling
ldastey-dev 8a75dab
fix(update): track non-markdown base files and check the tooling copies
ldastey-dev e4430d3
fix(deploy): preserve consumer manifest configuration on redeploy
ldastey-dev 59353d3
docs(core): use --quiet for the session-start update check
ldastey-dev 5e98b5d
fix(update): escape the substitution delimiter in manifest key lookup
ldastey-dev a5aa682
fix(update): treat index.md as mandatory payload content
ldastey-dev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| version: 2 | ||
|
|
||
| # Third-party actions are pinned to full commit SHAs so a retargeted tag cannot | ||
| # execute in our workflows. Dependabot is what keeps those pins current, so the | ||
| # hardening does not decay into running abandoned versions. | ||
| updates: | ||
| - package-ecosystem: github-actions | ||
| directory: / | ||
| schedule: | ||
| interval: monthly | ||
| commit-message: | ||
| # Workflow files are not deployable, so this never cuts a release. | ||
| prefix: ci | ||
| labels: | ||
| - dependencies |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| name: PR title | ||
|
|
||
| # The release job derives the version bump size from the merge commit subject, | ||
| # which is the pull request title when a PR is squashed. If the title is not a | ||
| # valid Conventional Commit the bump cannot be derived, so the title is checked | ||
| # on every pull request rather than discovered at merge time. | ||
| on: | ||
| pull_request: | ||
| types: [opened, edited, reopened, synchronize] | ||
|
|
||
| concurrency: | ||
| group: pr-title-${{ github.event.pull_request.number }} | ||
| cancel-in-progress: true | ||
|
|
||
| permissions: | ||
| pull-requests: read | ||
|
|
||
| jobs: | ||
| lint: | ||
| name: Conventional Commit title | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| # Pinned to a full commit SHA, not a mutable tag: a compromised or | ||
| # retargeted "v5" tag would otherwise execute here. Dependabot raises the | ||
| # upgrade, so pinning costs nothing in currency. | ||
| - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3 | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| with: | ||
| # This list is PR hygiene, not the bump decision. It keeps changelog | ||
| # groups meaningful by rejecting invented types before merge. | ||
| # | ||
| # next-version.sh deliberately does NOT share this list: it applies a | ||
| # patch floor to any unrecognised type, so a deployable change that | ||
| # reaches main by some route this lint never saw - a direct push, or | ||
| # a non-squash merge - still cuts a release instead of being lost. | ||
| # The lint prevents unknown types; the floor makes them harmless. | ||
| types: | | ||
| feat | ||
| fix | ||
| docs | ||
| style | ||
| refactor | ||
| perf | ||
| test | ||
| build | ||
| ci | ||
| chore | ||
| revert | ||
| requireScope: false | ||
| # A single-line subject keeps the squashed commit parseable. | ||
| subjectPattern: ^(?![A-Z])(?!.*\.$).+$ | ||
| subjectPatternError: | | ||
| The subject "{subject}" is invalid: it must start with a lower-case | ||
| letter and must not end with a full stop. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| name: Release | ||
|
|
||
| # Cuts a release when deployable content lands on main. This is the only writer | ||
| # of VERSION, CHANGELOG.md, tags and scripts/baselines/, which is what lets a | ||
| # deployed consumer trust a single 6-byte fetch of VERSION to detect staleness. | ||
| on: | ||
| push: | ||
| branches: [main] | ||
|
|
||
| # Releases must queue, never cancel. Cancelling an in-flight release could leave | ||
| # a tag pointing at a commit whose VERSION was never pushed. | ||
| concurrency: | ||
| group: release-main | ||
| cancel-in-progress: false | ||
|
|
||
| permissions: | ||
| contents: write | ||
|
|
||
| jobs: | ||
| release: | ||
| name: Cut release | ||
| runs-on: ubuntu-latest | ||
| # Loop guard, belt and braces: the release commit is authored by the Actions | ||
| # bot and carries [skip ci]. Either condition alone would do; both together | ||
| # mean a change to one convention cannot cause an infinite release loop. | ||
| if: >- | ||
| github.actor != 'github-actions[bot]' && | ||
| !contains(github.event.head_commit.message, '[skip ci]') | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 | ||
| # Persist the token so the later push and tag reuse it. | ||
| persist-credentials: true | ||
|
|
||
| - name: Resolve changed files for this push | ||
| env: | ||
| BEFORE: ${{ github.event.before }} | ||
| AFTER: ${{ github.sha }} | ||
| run: | | ||
| set -euo pipefail | ||
| # github.event.before is all-zeroes for the first push to a new branch | ||
| # and can point at a commit that no longer exists after a force-push. | ||
| # Fall back to the first-parent diff, which is the squashed merge. | ||
| if [ -z "${BEFORE//0/}" ] || ! git cat-file -e "$BEFORE^{commit}" 2>/dev/null; then | ||
| git show --name-only --pretty=format: "$AFTER" > /tmp/changed-files.txt | ||
| else | ||
| git diff --name-only "$BEFORE" "$AFTER" > /tmp/changed-files.txt | ||
| fi | ||
| grep -v '^$' /tmp/changed-files.txt > /tmp/changed-files.tmp || true | ||
| mv /tmp/changed-files.tmp /tmp/changed-files.txt | ||
| echo "Changed files:" | ||
| sed 's/^/ /' /tmp/changed-files.txt | ||
|
|
||
| - name: Compute next version | ||
| id: next | ||
| run: | | ||
| set -euo pipefail | ||
| subject="$(git log -1 --pretty=format:'%s')" | ||
| # Pass the whole message, not just the subject: Conventional Commits | ||
| # puts BREAKING CHANGE in the footer, so a subject-only read ships a | ||
| # breaking change as a patch to consumers pinned to a major line. | ||
| git log -1 --pretty=format:'%B' > /tmp/commit-body.txt | ||
| current="$(tr -d ' \t\r\n' < VERSION)" | ||
| prev_tag="$(git tag --list 'v*' --sort=-v:refname | head -n 1 || true)" | ||
|
|
||
| # The bump size is read from the merge subject, which equals the PR | ||
| # title only under squash-merge. Under a merge commit the subject is | ||
| # "Merge pull request #..." and under rebase-merge it is the last | ||
| # commit's subject, so pr-title.yml would be validating a string that | ||
| # never reaches this job. Warn loudly rather than silently defaulting | ||
| # to a patch, which would understate a feat or a breaking change. | ||
| if ! printf '%s' "$subject" | grep -Eq '^[a-zA-Z]+(\([^)]*\))?!?:'; then | ||
| echo "::warning::Merge subject '$subject' is not a Conventional Commit. \ | ||
| The repository must use squash-merge so the PR title becomes the merge subject; \ | ||
| falling back to the patch floor." | ||
| { | ||
| echo "> **Merge subject is not a Conventional Commit.**" | ||
| echo "> \`$subject\`" | ||
| echo ">" | ||
| echo "> Bump size fell back to the patch floor. Enable squash-merge only." | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||
| fi | ||
|
|
||
| next="$(scripts/ci/next-version.sh --changed-files /tmp/changed-files.txt --subject "$subject" --body-file /tmp/commit-body.txt --latest-tag "$prev_tag")" | ||
| echo "current=$current" >> "$GITHUB_OUTPUT" | ||
| echo "next=$next" >> "$GITHUB_OUTPUT" | ||
| echo "prev_tag=$prev_tag" >> "$GITHUB_OUTPUT" | ||
| if [ "$next" = "none" ]; then | ||
| echo "No deployable content changed. Skipping release." >> "$GITHUB_STEP_SUMMARY" | ||
| elif [ -z "$prev_tag" ]; then | ||
| echo "Initial release: publishing $next as-is." >> "$GITHUB_STEP_SUMMARY" | ||
| else | ||
| echo "Releasing $current -> $next" >> "$GITHUB_STEP_SUMMARY" | ||
| fi | ||
|
|
||
| - name: Fail if the release tag already exists | ||
| if: steps.next.outputs.next != 'none' | ||
| env: | ||
| NEXT: ${{ steps.next.outputs.next }} | ||
| run: | | ||
| set -euo pipefail | ||
| # Re-releasing an existing version would move a tag consumers have | ||
| # already pinned to, so stop rather than overwrite. | ||
| if git rev-parse -q --verify "refs/tags/v$NEXT" >/dev/null; then | ||
| echo "::error::Tag v$NEXT already exists. Refusing to re-release it." | ||
| exit 1 | ||
| fi | ||
|
|
||
| - name: Write VERSION, CHANGELOG and baseline | ||
| if: steps.next.outputs.next != 'none' | ||
| env: | ||
| NEXT: ${{ steps.next.outputs.next }} | ||
| PREV_TAG: ${{ steps.next.outputs.prev_tag }} | ||
| run: | | ||
| set -euo pipefail | ||
| printf '%s\n' "$NEXT" > VERSION | ||
| # The baseline must hash the released content, so write it after | ||
| # VERSION but from the same working tree that is about to be tagged. | ||
| scripts/ci/write-baseline.sh "$NEXT" | ||
| scripts/ci/write-changelog.sh "$NEXT" "$PREV_TAG" | ||
|
|
||
| - name: Commit, then tag that commit | ||
| if: steps.next.outputs.next != 'none' | ||
| env: | ||
| NEXT: ${{ steps.next.outputs.next }} | ||
| run: | | ||
| set -euo pipefail | ||
| git config user.name 'github-actions[bot]' | ||
| git config user.email '41898282+github-actions[bot]@users.noreply.github.com' | ||
| git add VERSION CHANGELOG.md "scripts/baselines/$NEXT.sha256" | ||
|
|
||
| # On an initial release VERSION and the baseline are already correct | ||
| # in the tree, so there may be nothing to commit. That is not a | ||
| # failure - the tag still has to be created against this commit. | ||
| if git diff --cached --quiet; then | ||
| echo "Nothing to commit; tagging the current commit." | ||
| else | ||
| git commit -m "chore(release): $NEXT [skip ci]" | ||
|
|
||
| # Push the commit before tagging it. Tagging first would produce a | ||
| # tag whose tree still holds the previous VERSION, so a consumer | ||
| # resolving the tag would download content that disagrees with the | ||
| # version they were told to expect. | ||
| # | ||
| # Another release may have landed while this job queued, so rebase | ||
| # and retry rather than failing the release outright. | ||
| attempt=1 | ||
| until git push origin HEAD:main; do | ||
| if [ "$attempt" -ge 3 ]; then | ||
| echo "::error::Could not push the release commit after $attempt attempts." | ||
| exit 1 | ||
| fi | ||
| echo "Push rejected; rebasing onto the latest main (attempt $attempt)." | ||
| git fetch origin main | ||
| git rebase origin/main | ||
| attempt=$((attempt + 1)) | ||
| done | ||
| fi | ||
|
|
||
| git tag -a "v$NEXT" -m "v$NEXT" HEAD | ||
| git push origin "v$NEXT" | ||
|
|
||
| - name: Publish GitHub release | ||
| if: steps.next.outputs.next != 'none' | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| NEXT: ${{ steps.next.outputs.next }} | ||
| run: | | ||
| set -euo pipefail | ||
| # Extract just this version's section from the changelog for the notes. | ||
| awk -v ver="## $NEXT " ' | ||
| index($0, ver) == 1 { inside = 1; next } | ||
| /^## / && inside { exit } | ||
| inside { print } | ||
| ' CHANGELOG.md > /tmp/notes.md | ||
| [ -s /tmp/notes.md ] || echo "See CHANGELOG.md." > /tmp/notes.md | ||
| gh release create "v$NEXT" \ | ||
| --title "v$NEXT" \ | ||
| --notes-file /tmp/notes.md \ | ||
| "scripts/baselines/$NEXT.sha256#baseline-$NEXT.sha256" | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.