Skip to content

fix: sanitize GitHub Actions output in maintenance workflow#1070

Merged
GrammaTonic merged 2 commits intomainfrom
copilot/sanitize-version-extraction
Dec 4, 2025
Merged

fix: sanitize GitHub Actions output in maintenance workflow#1070
GrammaTonic merged 2 commits intomainfrom
copilot/sanitize-version-extraction

Conversation

Copy link
Contributor

Copilot AI commented Dec 4, 2025

📋 Pull Request Description

🔀 Merge Strategy

This repository uses SQUASH MERGE as the standard merge strategy.

Why Squash Merge?

  • Clean, linear commit history on main branch - easier to understand project evolution
  • One commit per feature/fix - easier rollbacks and cherry-picking
  • Better release notes - automated changelog generation from squashed commits
  • Simplified CI/CD - cleaner git history for automated release processes
  • Consistent with Dependabot - auto-merge configuration uses squash strategy
  • Reduced noise - no "fix typo" or "address review comments" commits in main
  • Easier bisecting - each commit represents a complete, logical change

How to Create a PR (Recommended):

# Create PR using a markdown file for detailed description
gh pr create --base develop --fill-first --body-file .github/pull_request_template.md

# Or for quick PRs with inline body:
gh pr create --base develop --title "feat: your feature title" --body "Description here"

# For promotion PRs (develop → main):
gh pr create --base main --head develop --title "chore: promote develop to main" --body-file PR_DESCRIPTION.md

How to Merge (Recommended):

# Via GitHub CLI (recommended - ensures squash merge):
gh pr merge <PR_NUMBER> --squash --delete-branch --body "Squash merge: <brief summary>"

# Via GitHub Web UI:
# 1. Click "Squash and merge" button (NOT "Merge pull request" or "Rebase and merge")
# 2. Edit the commit message if needed
# 3. Confirm the merge
# 4. Delete the branch

⚠️ CRITICAL: After squash merging to main, you MUST back-sync develop (see Post-Merge Back-Sync section below).

⚠️ Pre-Submission Checklist

Branch Sync Requirements:

  • I have pulled the latest changes from main branch: git pull origin main
  • I have pulled the latest changes from develop branch: git pull origin develop
  • I have rebased my feature branch on the target branch (if applicable)
  • My branch is up-to-date with no merge conflicts

Quick sync commands:

# Fetch all remote branches
git fetch --all

# Update local main branch
git checkout main
git pull origin main

# Update local develop branch
git checkout develop
git pull origin develop

# Return to your feature branch and rebase (if needed)
git checkout <your-feature-branch>
git rebase develop  # or 'main' depending on your target branch

Post-Merge Back-Sync (CRITICAL after squash merging to main):

⚠️ MANDATORY STEP - DO NOT SKIP THIS!

Why is this needed?
When you squash merge a PR from develop to main, the individual commits from develop are condensed into a single commit on main. This causes develop to appear "ahead" of main in git history, even though the code is identical. The back-sync merge resolves this divergence and prevents:

  • ❌ Incorrect "X commits ahead" status on develop
  • ❌ Merge conflicts on subsequent PRs
  • ❌ CI/CD pipeline confusion
  • ❌ Duplicate commits in future merges

When to perform back-sync:

  • ALWAYS after merging a promotion PR (developmain) with squash merge
  • ALWAYS after merging any PR directly to main with squash merge
  • IMMEDIATELY after the squash merge completes (don't wait!)
  • ❌ NOT needed when merging feature branches to develop (develop will be promoted later)

How to perform back-sync:

# Step 1: Ensure your local branches are up-to-date
git fetch --all

# Step 2: Switch to develop and pull latest
git checkout develop
git pull origin develop

# Step 3: Merge main back into develop (creates a merge commit)
git merge main -m "chore: sync develop with main after squash merge"

# Step 4: Push the back-sync to remote
git push origin develop

# This ensures develop stays in sync with main after squash merges
# The merge commit preserves the development history in develop
# while keeping main's linear squashed history

Alternative (using GitHub CLI):

# Create a back-sync PR (for teams requiring PR workflow)
git checkout develop
git pull origin develop
git checkout -b chore/backsync-main-to-develop
git merge main -m "chore: sync develop with main after squash merge"
git push origin chore/backsync-main-to-develop
gh pr create --base develop --head chore/backsync-main-to-develop \
  --title "chore: back-sync main to develop after squash merge" \
  --body "Automatic back-sync after squash merging to main. This prevents 'ahead' status."
gh pr merge --merge --delete-branch  # Use regular merge, not squash!

Verification:

# After back-sync, these commands should show no differences:
git diff main..develop  # Should be empty (no code differences)
git log --oneline main..develop  # Should only show merge commits (no unique commits)

# Check branch status (should show "up to date"):
git checkout develop
git status
# Should NOT say "Your branch is ahead of 'origin/develop'"

Troubleshooting:

# If you forgot to back-sync and now have conflicts:
git checkout develop
git pull origin develop
git fetch origin main
git merge origin/main -m "chore: late back-sync after squash merge"
# Resolve any conflicts, then:
git push origin develop

Summary

Maintenance workflow fails with Unable to process file command 'output' successfully. Invalid format when version extraction produces empty values. Direct writes to $GITHUB_OUTPUT break on empty strings, newlines, or special characters.

Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📚 Documentation update
  • 🔧 Configuration change
  • 🧪 Test improvements
  • 🚀 Performance improvement
  • 🔒 Security enhancement

Related Issues

Resolves workflow run failure: https://github.com/GrammaTonic/github-runner/actions/runs/19915664449/job/57093663236

🔄 Changes Made

Files Modified

  • .github/workflows/maintenance.yml - Added sanitization helper for version extraction steps

Key Changes

Added write_output() helper function:

  • Strips carriage returns and collapses newlines to spaces
  • Trims whitespace and writes "unknown" for empty values
  • Guarantees single-line key=value format for $GITHUB_OUTPUT

Applied to two steps:

  • extract-versions: Sanitizes RUNNER_VERSION, NODE_VERSION, PLAYWRIGHT_VERSION, CYPRESS_VERSION, FLAT_VERSION
  • check-updates: Sanitizes LATEST_RUNNER, LATEST_NODE, runner-needs-update flag

Additional hardening:

  • Added 2>/dev/null to grep commands (suppresses errors on missing files)
  • Added head -n1 to RUNNER_VERSION extraction (consistency)
  • Fixed NODE_VERSION to avoid .x suffix when pattern doesn't match

Before:

FLAT_VERSION=$(grep -E 'flat@[0-9]+\.[0-9]+\.[0-9]+' docker/Dockerfile* | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' | head -1)
echo "flat-version=$FLAT_VERSION" >> $GITHUB_OUTPUT
# Result when pattern doesn't match: "flat-version=" (invalid format error)

After:

write_output() {
  name="$1"
  value="$2"
  value=$(printf "%s" "$value" | tr -d '\r' | awk 'BEGIN{ORS="";} {gsub(/^[ \t]+|[ \t]+$/,""); if (NR>1) printf " "; print $0}')
  value=$(printf "%s" "$value" | tr '\n' ' ')
  [[ -z "$value" ]] && value="unknown"
  printf "%s=%s\n" "$name" "$value" >> "$GITHUB_OUTPUT"
}

FLAT_VERSION=$(grep -E 'flat@[0-9]+\.[0-9]+\.[0-9]+' docker/Dockerfile* 2>/dev/null | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' | head -1)
write_output "flat-version" "$FLAT_VERSION"
# Result when pattern doesn't match: "flat-version=unknown" (valid format)

🧪 Testing

Testing Performed

  • Unit tests pass
  • Integration tests pass
  • Manual testing completed
  • Docker build successful
  • Chrome runner tested (if applicable)

Test Coverage

  • New tests added for new functionality
  • Existing tests updated
  • All tests are passing

Manual Testing Steps

  1. Tested write_output() with edge cases: empty values, newlines, carriage returns, whitespace
  2. Ran actual extraction against Dockerfiles
  3. Validated all outputs are single-line key=value format

Test results:

runner-version=2.329.0
node-version=unknown          # Pattern doesn't match (was ".x")
playwright-version=unknown    # Pattern doesn't match (was empty)
cypress-version=13.15.0
flat-version=unknown          # Pattern doesn't match (was empty - caused original error)

📸 Screenshots/Demos

N/A - Workflow fix, no UI changes.

🔒 Security Considerations

  • No new security vulnerabilities introduced
  • Secrets/tokens handled appropriately
  • Container security best practices followed

📚 Documentation

  • README.md updated
  • Documentation in docs/ updated
  • Wiki pages updated
  • Code comments added/updated
  • API documentation updated

🚀 Deployment Notes

  • No deployment changes required
  • Docker image rebuild required
  • Environment variables updated
  • Configuration changes needed

✅ Checklist

  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published

🤖 AI Review Request

/cc @copilot


Note for Reviewers:

  • Helper function prevents format errors when grep patterns don't match
  • All version extractions now fail-safe with "unknown" fallback
  • Workflow will no longer crash on missing package versions
Original prompt

Problem:
The maintenance workflow at .github/workflows/maintenance.yml writes parsed version values to $GITHUB_OUTPUT without sanitization. This caused the job to fail with:

Unable to process file command 'output' successfully.
Invalid format '2.329.0'

Root cause:
Values parsed from Dockerfiles (e.g., FLAT_VERSION) can be empty, contain newlines, or unexpected characters which break the GitHub Actions output file format (expects key=value single-line entries).

Goal:
Modify the version extraction step in the version-tracking-update job to sanitize extracted values and write safe single-line outputs to $GITHUB_OUTPUT. Add a small helper function write_output that:

  • strips CR and collapses newlines
  • trims whitespace
  • ensures the value is single-line
  • writes a fallback value ("unknown") when empty

Actionable changes to apply:

  • Update the step with id: extract-versions in .github/workflows/maintenance.yml to the safer implementation below. This replaces the current run: block for that step.

Replacement run block (exact content to be inserted under the extract-versions step):

|
| echo "Extracting versions from Docker images..."
|
| # helper: sanitize and write single-line output
| write_output() {
| name="$1"
| value="$2"
| # remove carriage returns, collapse multiple lines to single space, trim leading/trailing whitespace
| value=$(printf "%s" "$value" | tr -d '\r' | awk 'BEGIN{ORS="";} {gsub(/^[ \t]+|[ \t]+$/,""); if (NR>1) printf " "; print $0}')
| # final safety: remove any literal newlines just in case
| value=$(printf "%s" "$value" | tr '\n' ' ')
| # if empty, set to "unknown"
| if [[ -z "$value" ]]; then
| value="unknown"
| fi
| # write in key=value single-line form
| printf "%s=%s\n" "$name" "$value" >> "$GITHUB_OUTPUT"
| }
|
| # Extract GitHub Actions Runner version (remove quotes)
| RUNNER_VERSION=$(grep -E 'ARG RUNNER_VERSION=' docker/Dockerfile | cut -d'=' -f2 | tr -d '"' | head -n1)
| write_output "runner-version" "$RUNNER_VERSION"
|
| # Extract Node.js version (first numeric major from setup_x.x pattern)
| NODE_VERSION=$(grep -E 'setup_[0-9]+.x' docker/Dockerfile | grep -o '[0-9]+' | head -1)
| write_output "node-version" "${NODE_VERSION}.x"
|
| # Extract testing framework versions from Chrome Dockerfile
| PLAYWRIGHT_VERSION=$(grep -E 'playwright@[0-9]+.[0-9]+.[0-9]+' docker/Dockerfile.chrome 2>/dev/null | grep -o '[0-9]+.[0-9]+.[0-9]+' | head -1)
| CYPRESS_VERSION=$(grep -E 'cypress@[0-9]+.[0-9]+.[0-9]+' docker/Dockerfile.chrome 2>/dev/null | grep -o '[0-9]+.[0-9]+.[0-9]+' | head -1)
| write_output "playwright-version" "$PLAYWRIGHT_VERSION"
| write_output "cypress-version" "$CYPRESS_VERSION"
|
| # Extract security patch versions
| FLAT_VERSION=$(grep -E 'flat@[0-9]+.[0-9]+.[0-9]+' docker/Dockerfile* 2>/dev/null | grep -o '[0-9]+.[0-9]+.[0-9]+' | head -1)
| write_output "flat-version" "$FLAT_VERSION"
|

Notes and tests:

  • The helper is conservative and writes "unknown" for missing values. If you prefer skipping outputs when missing, change the helper to return without writing when value is empty.
  • Apply the same pattern to any other steps that write arbitrary values to $GITHUB_OUTPUT (e.g., check-updates), to avoid future failures.

Reference:

Please create a new branch (named: fix/sanitize-github-output-maintenance) and open a pull request with the change. Do not merge the PR.

This pull request was created as a result of the following prompt from Copilot chat.

Problem:
The maintenance workflow at .github/workflows/maintenance.yml writes parsed version values to $GITHUB_OUTPUT without sanitization. This caused the job to fail with:

Unable to process file command 'output' successfully.
Invalid format '2.329.0'

Root cause:
Values parsed from Dockerfiles (e.g., FLAT_VERSION) can be empty, contain newlines, or unexpected characters which break the GitHub Actions output file format (expects key=value single-line entries).

Goal:
Modify the version extraction step in the version-tracking-update job to sanitize extracted values and write safe single-line outputs to $GITHUB_OUTPUT. Add a small helper function write_output that:

  • strips CR and collapses newlines
  • trims whitespace
  • ensures the value is single-line
  • writes a fallback value ("unknown") when empty

Actionable changes to apply:

  • Update the step with id: extract-versions in .github/workflows/maintenance.yml to the safer implementation below. This replaces the current run: block for that step.

Replacement run block (exact content to be inserted under the extract-versions step):

|
| echo "Extracting versions from Docker images..."
|
| # helper: sanitize and write single-line output
| write_output() {
| name="$1"
| value="$2"
| # remove carriage returns, collapse multiple lines to single space, trim leading/trailing whitespace
| value=$(printf "%s" "$value" | tr -d '\r' | awk 'BEGIN{ORS="";} {gsub(/^[ \t]+|[ \t]+$/,""); if (NR>1) printf " "; print $0}')
| # final safety: remove any literal newlines just in case
| value=$(printf "%s" "$value" | tr '\n' ' ')
| # if empty, set to "unknown"
| if [[ -z "$value" ]]; then
| value="unknown"
| fi
| # write in key=value single-line form
| printf "%s=%s\n" "$name" "$value" >> "$GITHUB_OUTPUT"
| }
|
| # Extract GitHub Actions Runner version (remove quotes)
| RUNNER_VERSION=$(grep -E 'ARG RUNNER_VERSION=' docker/Dockerfile | cut -d'=' -f2 | tr -d '"' | head -n1)
| write_output "runner-version" "$RUNNER_VERSION"
|
| # Extract Node.js version (first numeric major from setup_x.x pattern)
| NODE_VERSION=$(grep -E 'setup_[0-9]+.x' docker/Dockerfile | grep -o '[0-9]+' | head -1)
| write_output "node-version" "${NODE_VERSION}.x"
|
| # Extract testing framework versions from Chrome Dockerfile
| PLAYWRIGHT_VERSION=$(grep -E 'playwright@[0-9]+.[0-9]+.[0-9]+' docker/Dockerfile.chrome 2>/dev/null | grep -o '[0-9]+.[0-9]+.[0-9]+' | head -1)
| CYPRESS_VERSION=$(grep -E 'cypress@[0-9]+.[0-9]+.[0-9]+' docker/Dockerfile.chrome 2>/dev/null | grep -o '[0-9]+.[0-9]+.[0-9]+' | head -1)
| write_output "playwright-version" "$PLAYWRIGHT_VERSION"
| write_output "cypress-version" "$CYPRESS_VERSION"
|
| # Extract security patch versions
| FLAT_VERSION=$(grep -E 'flat@[0-9]+.[0-9]+.[0-9]+' docker/Dockerfile* 2>/dev/null | grep -o '[0-9]+.[0-9]+.[0-9]+' | head -1)
| write_output "flat-version" "$FLAT_VERSION"
|

Notes and tests:

  • The helper is conservative and writes "unknown" for missing values. If you prefer skipping outputs when missing, change the helper to return without writing when value is empty.
  • Apply the same pattern to any other steps that write arbitrary values to $GITHUB_OUTPUT (e.g., check-updates), to avoid future failures.

Reference:

Please create a new branch (named: fix/sanitize-github-output-maintenance) and open a pull request with the change. Do not merge the PR.


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

- Add write_output() helper function to sanitize version values
- Removes CR, collapses newlines, trims whitespace
- Writes "unknown" for empty values to prevent format errors
- Applied to both extract-versions and check-updates steps
- Fixes "Unable to process file command 'output' successfully" error
- Prevents issues when grep patterns don't match (e.g., flat@, playwright@)

Resolves failing workflow run at:
https://github.com/GrammaTonic/github-runner/actions/runs/19915664449/job/57093663236

Co-authored-by: GrammaTonic <8269379+GrammaTonic@users.noreply.github.com>
Copilot AI changed the title [WIP] Modify version extraction to sanitize outputs in maintenance workflow fix: sanitize GitHub Actions output in maintenance workflow Dec 4, 2025
Copilot AI requested a review from GrammaTonic December 4, 2025 21:28
@GrammaTonic GrammaTonic marked this pull request as ready for review December 4, 2025 21:30
Repository owner deleted a comment from gemini-code-assist bot Dec 4, 2025
@GrammaTonic GrammaTonic merged commit 04a07d7 into main Dec 4, 2025
28 of 42 checks passed
@GrammaTonic GrammaTonic deleted the copilot/sanitize-version-extraction branch December 4, 2025 21:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants