Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 64 additions & 51 deletions .github/workflows/changerawr-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,63 +27,76 @@ jobs:
id: prepare_notes
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
set -eo pipefail

# Function to extract release notes from PR body
extract_release_notes() {
local body="$1"

# Remove "Summary by CodeRabbit" section and auto-generated comment line
local cleaned_body="$(printf '%s\n' "$body" \
| grep -v '<!-- end of auto-generated comment: release notes by coderabbit.ai -->' \
| awk '
BEGIN { skip=0 }
/^## Summary by CodeRabbit/ { skip=1; next }
/^## / && skip==1 { skip=0 }
skip==0 { print }
')"

# Try to extract content under "## Release Notes" heading
local notes="$(printf '%s\n' "$cleaned_body" \
| awk 'f && /^## /{exit} /^## Release Notes/{f=1; next} f')"

# If no specific section found, use the entire cleaned body
if [ -z "$notes" ]; then
notes="$cleaned_body"
fi

printf '%s\n' "$notes"
}

echo "Fetching PR #${{ github.event.pull_request.number }} details..."

set -euo pipefail

echo "Fetching PR #${PR_NUMBER} details..."

# Fetch the PR body using GitHub CLI
PR_BODY=$(gh pr view "${{ github.event.pull_request.number }}" --json body --jq '.body' 2>/dev/null || echo "")

NOTES=""
if [ -n "$PR_BODY" ]; then
echo "PR body found, extracting release notes..."
NOTES="$(extract_release_notes "$PR_BODY")"
fi

# Fallback to PR title and recent commits if no body found
if [ -z "$NOTES" ] || [ "$NOTES" = "" ]; then
echo "No PR body found, using PR title and commits..."
NOTES="## ${{ github.event.pull_request.title }}"
PR_BODY="$(gh pr view "$PR_NUMBER" --json body --jq '.body' 2>/dev/null || echo "")"
export PR_BODY

# Strip CodeRabbit content and pull out the Release Notes section
NOTES="$(python3 <<'PY'
import os, re

body = os.environ.get("PR_BODY", "")

# 1. Drop the whole CodeRabbit auto-generated block (start marker .. end marker)
body = re.sub(
r"<!--\s*This is an auto-generated comment: release notes by coderabbit\.ai\s*-->"
r".*?"
r"<!--\s*end of auto-generated comment: release notes by coderabbit\.ai\s*-->",
"",
body,
flags=re.IGNORECASE | re.DOTALL,
)

# 2. Drop any remaining HTML comments (covers unpaired/renamed markers)
body = re.sub(r"<!--.*?-->", "", body, flags=re.DOTALL)

# 3. Drop a leftover "Summary by CodeRabbit" section: heading -> next h1/h2 or EOF
body = re.sub(
r"^#{1,6}\s*Summary by CodeRabbit\b.*?(?=^#{1,2}\s|\Z)",
"",
body,
flags=re.IGNORECASE | re.MULTILINE | re.DOTALL,
)

# 4. Prefer an explicit "Release Notes" section when the author wrote one
m = re.search(
r"^#{1,6}\s*Release Notes\s*$\n(?P<notes>.*?)(?=^#{1,2}\s|\Z)",
body,
flags=re.IGNORECASE | re.MULTILINE | re.DOTALL,
)
notes = m.group("notes") if m and m.group("notes").strip() else body

# Collapse runs of blank lines and trim
notes = re.sub(r"\n{3,}", "\n\n", notes).strip()
print(notes)
PY
)"

# Fallback to PR title and recent commits if nothing usable remains
if [ -z "${NOTES//[[:space:]]/}" ]; then
echo "No usable PR body found, using PR title and commits..."
NOTES="## ${PR_TITLE}"
NOTES="$NOTES"$'\n\n'"$(git log -n 5 --pretty=format:'- %s')"
fi

# Save to file and environment
echo "$NOTES" > release_notes.txt

# For multiline output, use delimiter

# Save to file and step output
printf '%s\n' "$NOTES" > release_notes.txt

# For multiline output, use a delimiter that cannot appear in the content
DELIM="RELEASE_NOTES_EOF_$(head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n')"
{
echo 'RELEASE_NOTES<<EOF'
echo "$NOTES"
echo 'EOF'
echo "RELEASE_NOTES<<${DELIM}"
printf '%s\n' "$NOTES"
echo "${DELIM}"
} >> "$GITHUB_OUTPUT"

echo "Release notes prepared:"
cat release_notes.txt

Expand Down
44 changes: 35 additions & 9 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,17 +142,43 @@ jobs:
});
const pr = prs.data.find(p => p.merged_at);
const fs = require('fs');
if (pr) {
const body = (pr.body || '')
.replace(/##\s*Summary by CodeRabbit[\s\S]*/i, '')

// Removes a leftover "Summary by CodeRabbit" section that survived
// marker stripping. Ends at the next heading or horizontal rule so a
// summary placed above the author's description cannot eat the body.
const stripSummarySection = (text) => {
const out = [];
let skip = false;
for (const line of text.split('\n')) {
if (/^#{1,6}\s*Summary by CodeRabbit\b/i.test(line)) { skip = true; continue; }
if (skip && (/^#{1,6}\s/.test(line) || /^-{3,}\s*$/.test(line))) skip = false;
if (!skip) out.push(line);
}
return out.join('\n');
};

const cleanBody = (raw) => {
let text = raw || '';
// 1. Drop the whole CodeRabbit auto-generated block (start marker .. end marker)
text = text.replace(
/<!--\s*This is an auto-generated comment: release notes by coderabbit\.ai\s*-->[\s\S]*?<!--\s*end of auto-generated comment: release notes by coderabbit\.ai\s*-->/gi,
''
);
// 2. Drop any remaining HTML comments (covers unpaired/renamed markers)
text = text.replace(/<!--[\s\S]*?-->/g, '');
// 3. Drop a leftover CodeRabbit summary section
text = stripSummarySection(text);
// 4. Existing wording normalisation
text = text
.replace(/^(#+\s*)(Pull Request Description|PR Description)\s*$/gim, '$1Summary')
.replace(/Pull Request Description|PR Description/gi, 'Summary')
.replace(/\bPR\b/g, 'Release')
.trim() || 'No release notes provided.';
fs.writeFileSync('release_notes.md', body);
} else {
fs.writeFileSync('release_notes.md', 'No release notes provided.');
}
.replace(/\bPR\b/g, 'Release');
// 5. Collapse blank-line runs and trim
return text.replace(/\n{3,}/g, '\n\n').trim();
};

const body = pr ? cleanBody(pr.body) : '';
fs.writeFileSync('release_notes.md', body || 'No release notes provided.');

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
Expand Down
1 change: 1 addition & 0 deletions Core/Resgrid.Model/MapLayerData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
namespace Resgrid.Model
{
//[JsonObject]
[BsonIgnoreExtraElements]
public class MapLayerData //: BsonDocument
{
[BsonElement("type")]
Expand Down
6 changes: 6 additions & 0 deletions Core/Resgrid.Model/MapLayerDataFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
namespace Resgrid.Model
{
//[JsonObject]
// BsonNoId: the Id property below is a GeoJSON feature id, not a Mongo document id. Without this
// the driver's NamedIdMemberConvention promotes it to the class id member, which historically
// persisted it as "_id" on the embedded document. BsonIgnoreExtraElements lets those older
// documents still deserialize.
[BsonNoId]
[BsonIgnoreExtraElements]
public class MapLayerDataFeature
{
[BsonElement("type")]
Expand Down
1 change: 1 addition & 0 deletions Core/Resgrid.Model/MapLayerDataGeometry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
namespace Resgrid.Model
{
[JsonObject]
[BsonIgnoreExtraElements]
public class MapLayerDataGeometry
{
[BsonElement("type")]
Expand Down
1 change: 1 addition & 0 deletions Core/Resgrid.Model/MapLayerDataProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
namespace Resgrid.Model
{
//[JsonObject]
[BsonIgnoreExtraElements]
public class MapLayerDataProperties
{
[BsonElement("shape")]
Expand Down
7 changes: 7 additions & 0 deletions Core/Resgrid.Model/Services/IShiftsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ public interface IShiftsService
/// <returns>Task&lt;Shift&gt;.</returns>
Task<Shift> SaveShiftAsync(Shift shift, CancellationToken cancellationToken = default(CancellationToken));

/// <summary>Updates just the shift's start day, without cascading into its child collections.</summary>
/// <param name="shift">The shift.</param>
/// <param name="startDay">The day the shift starts on.</param>
/// <param name="cancellationToken">The cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>Task&lt;Shift&gt;.</returns>
Task<Shift> UpdateShiftStartDayAsync(Shift shift, DateTime startDay, CancellationToken cancellationToken = default(CancellationToken));


/// <summary>Updates the shift personnel.</summary>
/// <param name="shift">The shift.</param>
Expand Down
37 changes: 35 additions & 2 deletions Core/Resgrid.Services/ShiftsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,19 @@ public async Task<Shift> PopulateShiftData(Shift shift, bool getDepartment, bool
return await _shiftsRepository.SaveOrUpdateAsync(shift, cancellationToken);
}

public async Task<Shift> UpdateShiftStartDayAsync(Shift shift, DateTime startDay, CancellationToken cancellationToken = default(CancellationToken))
{
if (shift == null)
return null;

shift.StartDay = startDay;

// firstLevelOnly: Days, Groups, Personnel and Admins are each managed by their own
// methods. A cascading save would rewrite those child rows from whatever happens to be
// loaded on this instance, which is not what a StartDay update should touch.
return await _shiftsRepository.SaveOrUpdateAsync(shift, cancellationToken, true);
}

public async Task<List<ShiftGroup>> GetShiftGroupsForShift(int shiftId)
{
var groups = await _shiftGroupsRepository.GetShiftGroupsByShiftIdAsync(shiftId);
Expand Down Expand Up @@ -143,19 +156,27 @@ public async Task<List<ShiftGroup>> GetShiftGroupsForShift(int shiftId)

public async Task<bool> UpdateShiftDatesAsync(Shift shift, List<ShiftDay> days, CancellationToken cancellationToken = default(CancellationToken))
{
if (shift == null)
return false;

// A shift with no days yet deserializes with Days null rather than an empty collection,
// which is the normal state the first time days are added to a shift.
var existingDays = shift.Days ?? new List<ShiftDay>();
days = days ?? new List<ShiftDay>();

// Adding Days
foreach (var day in days)
{
// Don't re-add days already that are apart of the shift
if (!shift.Days.Any(x => x.Day.Day == day.Day.Day && x.Day.Month == day.Day.Month && x.Day.Year == day.Day.Year))
if (!existingDays.Any(x => x.Day.Day == day.Day.Day && x.Day.Month == day.Day.Month && x.Day.Year == day.Day.Year))
{
day.ShiftId = shift.ShiftId;
await _shiftDaysRepository.SaveOrUpdateAsync(day, cancellationToken);
}
}

// Removing Days
var daysToRemove = from sd in shift.Days
var daysToRemove = from sd in existingDays
let day = days.FirstOrDefault(x => x.Day.Day == sd.Day.Day && x.Day.Month == sd.Day.Month && x.Day.Year == sd.Day.Year)
where day == null
select sd;
Expand Down Expand Up @@ -210,6 +231,9 @@ public async Task<List<ShiftGroup>> GetShiftGroupsForShift(int shiftId)
{
var trade = await GetShiftTradeByIdAsync(shiftTradeId);

if (trade?.Users == null)
return false;

var userTradeRequest = trade.Users.FirstOrDefault(x => x.UserId == userId);

if (userTradeRequest != null)
Expand All @@ -227,6 +251,9 @@ public async Task<List<ShiftGroup>> GetShiftGroupsForShift(int shiftId)
{
var trade = await GetShiftTradeByIdAsync(shiftTradeId);

if (trade?.Users == null)
return false;

var userTradeRequest = trade.Users.FirstOrDefault(x => x.UserId == userId);

if (userTradeRequest != null)
Expand Down Expand Up @@ -709,6 +736,12 @@ public async Task<List<ShiftSignupTrade>> GetOpenTradeRequestsForUserAsync(strin
public async Task<ShiftSignupTrade> GetShiftTradeByIdAsync(int shiftTradeId)
{
var trade = await _shiftSignupTradeRepository.GetByIdAsync(shiftTradeId);

// Without this the method throws on an unknown id instead of returning null, so callers
// have no way to handle a missing trade.
if (trade == null)
return null;

trade.Users = new List<ShiftSignupTradeUser>(await _shiftSignupTradeUserRepository.GetShiftSignupTradeUsersByTradeIdAsync(shiftTradeId));

return trade;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public string GetQuery()
{
var query = _sqlConfiguration.SelectDListMembersByUserQuery
.ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName,
_sqlConfiguration.DistributionListsTable,
_sqlConfiguration.DistributionListMembersTable,
_sqlConfiguration.ParameterNotation,
new string[] { "%USERID%" },
new string[] { "UserId" });
Expand Down
Loading
Loading