mirror of
https://github.com/gmeligio/flutter-docker-image.git
synced 2026-05-24 12:30:34 +00:00
Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a1d62ec22 | |||
| 7672004a29 | |||
| 10a162b392 | |||
| 40855b7abf | |||
| f81107fe7b | |||
| b5ad245466 | |||
| 3fb14b4fab | |||
| 5bbeeb282a | |||
| 4eb40b1786 | |||
| 008cce50a1 | |||
| 4203d45f27 | |||
| 668ec5041a | |||
| 87eb48fdc0 | |||
| 57cce30f4a | |||
| 6a853e8017 | |||
| 351ba21594 | |||
| 08dcebad40 | |||
| 135de6f707 | |||
| 7d16500293 | |||
| 2e2b0d91ef | |||
| 98fe8ee14c | |||
| a7ebaa85a7 | |||
| ae53ad7a4b | |||
| b8b0869a57 | |||
| d03dc18e78 | |||
| 792b91c445 | |||
| d301c28a1b | |||
| 3464097fbf | |||
| 40b632ca7f | |||
| 3c7b0968e3 | |||
| 846ffd66cb | |||
| 7759bf1b37 | |||
| fd97b2ba2d | |||
| 95063c618c | |||
| 9c23cccc58 | |||
| efa0acab8d | |||
| 91397e9d5d | |||
| 9f9f25d973 | |||
| 42ed24d66b | |||
| a150926917 | |||
| 9ca459f18f | |||
| f5a346fd44 | |||
| d2ad78c488 | |||
| 47fde777f9 | |||
| 984bd43ec3 | |||
| 2bcda57469 | |||
| a06f9699b2 | |||
| 063998e122 | |||
| 95eb215ee8 | |||
| 0e2d25235e | |||
| 4c89b17e47 |
@@ -0,0 +1,160 @@
|
||||
---
|
||||
model: claude-sonnet-4-6
|
||||
name: "OPSX: Apply"
|
||||
description: Implement tasks from an OpenSpec change (Experimental)
|
||||
category: Workflow
|
||||
tags: [workflow, artifacts, experimental]
|
||||
---
|
||||
|
||||
Implement tasks from an OpenSpec change.
|
||||
|
||||
**Input**: Optionally specify a change name (e.g., `/opsx:apply add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Select the change**
|
||||
|
||||
If a name is provided, use it. Otherwise:
|
||||
- Infer from conversation context if the user mentioned a change
|
||||
- Auto-select if only one active change exists
|
||||
- If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
|
||||
|
||||
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
|
||||
|
||||
2. **Check status to understand the schema**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
||||
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
||||
|
||||
3. **Get apply instructions**
|
||||
|
||||
```bash
|
||||
openspec instructions apply --change "<name>" --json
|
||||
```
|
||||
|
||||
This returns:
|
||||
- `contextFiles`: artifact ID -> array of concrete file paths (varies by schema)
|
||||
- Progress (total, complete, remaining)
|
||||
- Task list with status
|
||||
- Dynamic instruction based on current state
|
||||
|
||||
**Handle states:**
|
||||
- If `state: "blocked"` (missing artifacts): show message, suggest using `/opsx:continue`
|
||||
- If `state: "all_done"`: congratulate, suggest archive
|
||||
- Otherwise: proceed to implementation
|
||||
|
||||
4. **Read context files**
|
||||
|
||||
Read every file path listed under `contextFiles` from the apply instructions output.
|
||||
The files depend on the schema being used:
|
||||
- **spec-driven**: proposal, specs, design, tasks
|
||||
- Other schemas: follow the contextFiles from CLI output
|
||||
|
||||
5. **Show current progress**
|
||||
|
||||
Display:
|
||||
- Schema being used
|
||||
- Progress: "N/M tasks complete"
|
||||
- Remaining tasks overview
|
||||
- Dynamic instruction from CLI
|
||||
|
||||
6. **Implement tasks (loop until done or blocked)**
|
||||
|
||||
For each pending task:
|
||||
- Show which task is being worked on
|
||||
- Make the code changes required
|
||||
- Keep changes minimal and focused
|
||||
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
|
||||
- Continue to next task
|
||||
|
||||
<!-- opsx-git-commit-patch -->
|
||||
- **Git: Commit the task**
|
||||
```bash
|
||||
git add -A
|
||||
git diff --cached --quiet || git commit -m "feat(<n>): task N/M — <task description>"
|
||||
```
|
||||
|
||||
**Pause if:**
|
||||
- Task is unclear → ask for clarification
|
||||
- Implementation reveals a design issue → suggest updating artifacts
|
||||
- Error or blocker encountered → report and wait for guidance
|
||||
- User interrupts
|
||||
|
||||
7. **On completion or pause, show status**
|
||||
|
||||
Display:
|
||||
- Tasks completed this session
|
||||
- Overall progress: "N/M tasks complete"
|
||||
- If all done: suggest archive
|
||||
- If paused: explain why and wait for guidance
|
||||
|
||||
**Output During Implementation**
|
||||
|
||||
```
|
||||
## Implementing: <change-name> (schema: <schema-name>)
|
||||
|
||||
Working on task 3/7: <task description>
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
|
||||
Working on task 4/7: <task description>
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
```
|
||||
|
||||
**Output On Completion**
|
||||
|
||||
```
|
||||
## Implementation Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Progress:** 7/7 tasks complete ✓
|
||||
|
||||
### Completed This Session
|
||||
- [x] Task 1
|
||||
- [x] Task 2
|
||||
...
|
||||
|
||||
All tasks complete! You can archive this change with `/opsx:archive`.
|
||||
```
|
||||
|
||||
**Output On Pause (Issue Encountered)**
|
||||
|
||||
```
|
||||
## Implementation Paused
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Progress:** 4/7 tasks complete
|
||||
|
||||
### Issue Encountered
|
||||
<description of the issue>
|
||||
|
||||
**Options:**
|
||||
1. <option 1>
|
||||
2. <option 2>
|
||||
3. Other approach
|
||||
|
||||
What would you like to do?
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Keep going through tasks until done or blocked
|
||||
- Always read context files before starting (from the apply instructions output)
|
||||
- If task is ambiguous, pause and ask before implementing
|
||||
- If implementation reveals issues, pause and suggest artifact updates
|
||||
- Keep code changes minimal and scoped to each task
|
||||
- Update task checkbox immediately after completing each task
|
||||
- Pause on errors, blockers, or unclear requirements - don't guess
|
||||
- Use contextFiles from CLI output, don't assume specific file names
|
||||
|
||||
**Fluid Workflow Integration**
|
||||
|
||||
This skill supports the "actions on a change" model:
|
||||
|
||||
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
|
||||
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly
|
||||
@@ -0,0 +1,209 @@
|
||||
---
|
||||
model: claude-sonnet-4-6
|
||||
name: "OPSX: Archive"
|
||||
description: Archive a completed change in the experimental workflow
|
||||
category: Workflow
|
||||
tags: [workflow, archive, experimental]
|
||||
---
|
||||
|
||||
Archive a completed change in the experimental workflow.
|
||||
|
||||
**Input**: Optionally specify a change name after `/opsx:archive` (e.g., `/opsx:archive add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||
|
||||
Show only active changes (not already archived).
|
||||
Include the schema used for each change if available.
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Check artifact completion status**
|
||||
|
||||
Run `openspec status --change "<name>" --json` to check artifact completion.
|
||||
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used
|
||||
- `artifacts`: List of artifacts with their status (`done` or other)
|
||||
|
||||
**If any artifacts are not `done`:**
|
||||
- Display warning listing incomplete artifacts
|
||||
- Prompt user for confirmation to continue
|
||||
- Proceed if user confirms
|
||||
|
||||
3. **Check task completion status**
|
||||
|
||||
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
|
||||
|
||||
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
|
||||
|
||||
**If incomplete tasks found:**
|
||||
- Display warning showing count of incomplete tasks
|
||||
- Prompt user for confirmation to continue
|
||||
- Proceed if user confirms
|
||||
|
||||
**If no tasks file exists:** Proceed without task-related warning.
|
||||
|
||||
4. **Assess delta spec sync state**
|
||||
|
||||
Check for delta specs at `openspec/changes/<name>/specs/`. If none exist, proceed without sync prompt.
|
||||
|
||||
**If delta specs exist:**
|
||||
- Compare each delta spec with its corresponding main spec at `openspec/specs/<capability>/spec.md`
|
||||
- Determine what changes would be applied (adds, modifications, removals, renames)
|
||||
- Show a combined summary before prompting
|
||||
|
||||
**Always sync specs automatically** — do NOT prompt the user. If there are changes to sync, proceed directly.
|
||||
|
||||
Use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive after sync completes.
|
||||
|
||||
<!-- opsx-verify-scoring-patch -->
|
||||
|
||||
5. **Verify implementation**
|
||||
|
||||
Check for `.verify-passed` marker at `openspec/changes/<n>/.verify-passed`.
|
||||
|
||||
**If marker does NOT exist:**
|
||||
- Invoke Skill tool: `openspec-verify-change` for change `<n>`
|
||||
- Wait for the verdict:
|
||||
- **FAIL** → block archive, show score table, list CRITICAL issues
|
||||
- **CONDITIONAL** → show score table + warnings, ask user to confirm
|
||||
- **PASS** → write marker: `echo "passed" > "openspec/changes/<n>/.verify-passed"`, continue
|
||||
- Max 3 retry cycles: fix → re-verify → check verdict
|
||||
|
||||
**If marker EXISTS:** show "✓ Verified" and continue.
|
||||
|
||||
6. **Perform the archive**
|
||||
|
||||
Create the archive directory if it doesn't exist:
|
||||
```bash
|
||||
mkdir -p openspec/changes/archive
|
||||
```
|
||||
|
||||
Generate target name using current date: `YYYY-MM-DD-<change-name>`
|
||||
|
||||
**Check if target already exists:**
|
||||
- If yes: Fail with error, suggest renaming existing archive or using different date
|
||||
- If no: Move the change directory to archive
|
||||
|
||||
```bash
|
||||
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
||||
```
|
||||
|
||||
|
||||
<!-- opsx-git-commit-patch -->
|
||||
|
||||
7. **Git: Commit, push, and create PR**
|
||||
|
||||
Stage and commit the archived change:
|
||||
|
||||
```bash
|
||||
git add -A openspec/
|
||||
git diff --cached --quiet || git commit -m "docs: archive <change-name>"
|
||||
```
|
||||
|
||||
Push the branch and create a pull request:
|
||||
|
||||
```bash
|
||||
git push -u origin HEAD
|
||||
gh pr create --title "<change-name>" --body "<summary from the proposal>"
|
||||
```
|
||||
|
||||
8. **Watch pipeline and fix issues**
|
||||
|
||||
After creating the PR, monitor CI checks:
|
||||
|
||||
```bash
|
||||
gh pr checks <PR-number> --watch
|
||||
```
|
||||
|
||||
**If any check fails:**
|
||||
- Read the failure logs: `gh run view <run-id> --log-failed`
|
||||
- Identify the root cause and fix the issue
|
||||
- Commit the fix: `git add -A && git commit -m "fix(<change-name>): <brief description>"`
|
||||
- Push: `git push`
|
||||
- Re-check: `gh pr checks <PR-number> --watch`
|
||||
- Repeat until all checks pass (max 3 attempts, then report to user with failure details)
|
||||
|
||||
**If all checks pass:** notify the user and continue to summary.
|
||||
|
||||
9. **Display summary**
|
||||
|
||||
Show archive completion summary including:
|
||||
- Change name
|
||||
- Schema that was used
|
||||
- Archive location
|
||||
- Spec sync status (synced / sync skipped / no delta specs)
|
||||
- Note about any warnings (incomplete artifacts/tasks)
|
||||
|
||||
**Output On Success**
|
||||
|
||||
```
|
||||
## Archive Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
**Specs:** ✓ Synced to main specs
|
||||
|
||||
All artifacts complete. All tasks complete.
|
||||
```
|
||||
|
||||
**Output On Success (No Delta Specs)**
|
||||
|
||||
```
|
||||
## Archive Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
**Specs:** No delta specs
|
||||
|
||||
All artifacts complete. All tasks complete.
|
||||
```
|
||||
|
||||
**Output On Success With Warnings**
|
||||
|
||||
```
|
||||
## Archive Complete (with warnings)
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
**Specs:** Sync skipped (user chose to skip)
|
||||
|
||||
**Warnings:**
|
||||
- Archived with 2 incomplete artifacts
|
||||
- Archived with 3 incomplete tasks
|
||||
- Delta spec sync was skipped (user chose to skip)
|
||||
|
||||
Review the archive if this was not intentional.
|
||||
```
|
||||
|
||||
**Output On Error (Archive Exists)**
|
||||
|
||||
```
|
||||
## Archive Failed
|
||||
|
||||
**Change:** <change-name>
|
||||
**Target:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
|
||||
Target archive directory already exists.
|
||||
|
||||
**Options:**
|
||||
1. Rename the existing archive
|
||||
2. Delete the existing archive if it's a duplicate
|
||||
3. Wait until a different date to archive
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Always prompt for change selection if not provided
|
||||
- Use artifact graph (openspec status --json) for completion checking
|
||||
- Don't block archive on warnings - just inform and confirm
|
||||
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
|
||||
- Show clear summary of what happened
|
||||
- If sync is requested, use the Skill tool to invoke `openspec-sync-specs` (agent-driven)
|
||||
- If delta specs exist, always run the sync assessment and show the combined summary before prompting
|
||||
@@ -0,0 +1,149 @@
|
||||
---
|
||||
model: opus
|
||||
name: "OPSX: Explore"
|
||||
description: "Enter explore mode - think through ideas, investigate problems, clarify requirements"
|
||||
category: Workflow
|
||||
tags: [workflow, explore, experimental, thinking]
|
||||
---
|
||||
|
||||
<!-- opsx-explore-research-patch -->
|
||||
|
||||
Autonomous research mode. Investigate deeply. Visualize clearly. Deliver findings.
|
||||
|
||||
**IMPORTANT: Explore mode is for research and thinking, not implementing.** You may read files, search code, investigate the codebase, and search the web, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks — that's capturing thinking, not implementing.
|
||||
|
||||
**Input**: The argument after `/opsx:explore` is the topic to research. Examples:
|
||||
|
||||
- A feature idea: "adding rate limiting"
|
||||
- A technical question: "should we use Redis or SQLite for caching"
|
||||
- A problem: "the auth system is getting unwieldy"
|
||||
- A comparison: "postgres vs sqlite for this use case"
|
||||
- A change name: "add-dark-mode" (to research in context of that change)
|
||||
|
||||
---
|
||||
|
||||
## Research Workflow
|
||||
|
||||
### Phase 1: Accept Topic & Plan
|
||||
|
||||
1. **Receive topic** from the user
|
||||
2. **Check OpenSpec context** — run `openspec list --json` to find related changes; read their artifacts if relevant
|
||||
3. **Plan research strategy** — identify what to search on the web and what to investigate in the codebase. Do NOT share the plan with the user — just execute it.
|
||||
|
||||
### Phase 2: Autonomous Research
|
||||
|
||||
Execute all research without asking the user. Use every tool available:
|
||||
|
||||
1. **Web research** — Use `WebSearch` to find documentation, blog posts, examples, GitHub issues, API references, Stack Overflow answers. Use `WebFetch` to read full pages when search results look promising. Follow links to go deeper.
|
||||
2. **Codebase investigation** — Read files, search for patterns, map architecture, trace data flows relevant to the topic. Understand the current state before recommending changes.
|
||||
3. **Cross-reference** — Compare what the web says (best practices, library APIs, known issues) against what the codebase currently does. Identify gaps, outdated patterns, or opportunities.
|
||||
|
||||
**No questions rule:** You MUST exhaust web search and codebase investigation before considering asking the user. Only ask if the information is genuinely unfindable — business decisions, credentials, internal context not present in the code or on the web. If you do ask, state what you already tried.
|
||||
|
||||
### Phase 3: Deliver Structured Report
|
||||
|
||||
Present findings using this structure:
|
||||
|
||||
```
|
||||
## Research: <topic>
|
||||
|
||||
### Context
|
||||
What exists today in the codebase relevant to this topic.
|
||||
|
||||
┌────────────┐ ┌────────────┐
|
||||
│ Component │──────▶│ Component │
|
||||
│ A │ │ B │
|
||||
└────────────┘ └────────────┘
|
||||
|
||||
(Diagram the current architecture or data flow.)
|
||||
|
||||
### Findings
|
||||
What the research uncovered — key facts, patterns, constraints.
|
||||
Cite sources: URLs for web, file:line for code.
|
||||
|
||||
┌──────────────────────────────────┐
|
||||
│ Dependency Graph │
|
||||
│ │
|
||||
│ A ──▶ B ──▶ C │
|
||||
│ │ ▲ │
|
||||
│ └───────────┘ │
|
||||
└──────────────────────────────────┘
|
||||
|
||||
(Visualize relationships, data flows, or dependencies found.)
|
||||
|
||||
### Options
|
||||
2-3 approaches with tradeoffs.
|
||||
|
||||
| Approach | Pros | Cons |
|
||||
|----------|------|------|
|
||||
| Option A | ... | ... |
|
||||
| Option B | ... | ... |
|
||||
|
||||
OPTION A OPTION B
|
||||
┌──────────┐ ┌──────────┐
|
||||
│ Direct │ │ Via │
|
||||
│ path │ │ queue │
|
||||
└──────────┘ └──────────┘
|
||||
|
||||
(Side-by-side diagrams when comparing structural differences.)
|
||||
|
||||
### Recommendation
|
||||
The recommended path with justification.
|
||||
|
||||
BEFORE AFTER
|
||||
┌──────┐ ┌──────┐
|
||||
│ X │────▶ Y │ X │────▶ Z ────▶ Y
|
||||
└──────┘ └──────┘
|
||||
|
||||
(Diagram the proposed end-state — before vs after.)
|
||||
|
||||
### Open Questions (only if genuinely unanswerable)
|
||||
Questions that couldn't be resolved through research.
|
||||
Each must state what was already tried.
|
||||
|
||||
### Next Steps
|
||||
"Run /opsx:propose to create a change proposal" or similar.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Visualization
|
||||
|
||||
ASCII diagrams are a first-class element of the report, not an afterthought.
|
||||
|
||||
**"A good diagram is worth many paragraphs."**
|
||||
|
||||
- **Context** — Diagram the current architecture/flow relevant to the topic
|
||||
- **Findings** — Visualize relationships, data flows, or dependency graphs discovered during research
|
||||
- **Options** — Side-by-side diagrams comparing approaches when the difference is structural
|
||||
- **Recommendation** — Diagram the proposed end-state (before vs after)
|
||||
|
||||
Default to drawing when explaining structure, flow, or comparison. Use text when explaining reasoning or tradeoffs.
|
||||
|
||||
---
|
||||
|
||||
## OpenSpec Awareness
|
||||
|
||||
Check for existing context before researching:
|
||||
|
||||
```bash
|
||||
openspec list --json
|
||||
```
|
||||
|
||||
This tells you:
|
||||
|
||||
- If there are active changes related to the topic
|
||||
- Their names, schemas, and status
|
||||
- What artifacts already exist
|
||||
|
||||
If a related change exists, read its artifacts (`proposal.md`, `design.md`, `tasks.md`, specs) and reference them naturally in the report.
|
||||
|
||||
---
|
||||
|
||||
## Guardrails
|
||||
|
||||
- **No implementation** — Never write application code. Creating OpenSpec artifacts is fine if the user approves.
|
||||
- **No premature questions** — Whether asking the user or writing an "Open Question" in the report, exhaust the codebase and web first. If you can name a concrete next step (grep, file read, fetch) that would answer it, take that step instead.
|
||||
- **Cite sources** — Every finding must reference where it came from (URL for web, `file:line` for code). No unsourced claims.
|
||||
- **Stay grounded** — Prefer concrete evidence (code, docs, examples) over speculation. Label uncertain findings as such.
|
||||
- **Offer next steps, don't auto-proceed** — End with a recommendation for what to do next, but let the user decide.
|
||||
@@ -0,0 +1,129 @@
|
||||
---
|
||||
model: opus
|
||||
name: "OPSX: Propose"
|
||||
description: Propose a new change - create it and generate all artifacts in one step
|
||||
category: Workflow
|
||||
tags: [workflow, artifacts, experimental]
|
||||
---
|
||||
|
||||
Propose a new change - create the change and generate all artifacts in one step.
|
||||
|
||||
I'll create a change with artifacts:
|
||||
- proposal.md (what & why)
|
||||
- design.md (how)
|
||||
- tasks.md (implementation steps)
|
||||
|
||||
When ready to implement, run /opsx:apply
|
||||
|
||||
---
|
||||
|
||||
**Input**: The argument after `/opsx:propose` is the change name (kebab-case), OR a description of what the user wants to build.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no input provided, ask what they want to build**
|
||||
|
||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||
> "What change do you want to work on? Describe what you want to build or fix."
|
||||
|
||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||
|
||||
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||
|
||||
2. **Create the change directory**
|
||||
```bash
|
||||
openspec new change "<name>"
|
||||
```
|
||||
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
|
||||
|
||||
3. **Get the artifact build order**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to get:
|
||||
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
||||
- `artifacts`: list of all artifacts with their status and dependencies
|
||||
|
||||
4. **Create artifacts in sequence until apply-ready**
|
||||
|
||||
Use the **TodoWrite tool** to track progress through the artifacts.
|
||||
|
||||
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
||||
|
||||
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
||||
- Get instructions:
|
||||
```bash
|
||||
openspec instructions <artifact-id> --change "<name>" --json
|
||||
```
|
||||
- The instructions JSON includes:
|
||||
- `context`: Project background (constraints for you - do NOT include in output)
|
||||
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
||||
- `template`: The structure to use for your output file
|
||||
- `instruction`: Schema-specific guidance for this artifact type
|
||||
- `outputPath`: Where to write the artifact
|
||||
- `dependencies`: Completed artifacts to read for context
|
||||
- Read any completed dependency files for context
|
||||
- Create the artifact file using `template` as the structure
|
||||
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
||||
- Show brief progress: "Created <artifact-id>"
|
||||
|
||||
b. **Continue until all `applyRequires` artifacts are complete**
|
||||
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
||||
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
||||
- Stop when all `applyRequires` artifacts are done
|
||||
|
||||
c. **If an artifact requires user input** (unclear context):
|
||||
- Use **AskUserQuestion tool** to clarify
|
||||
- Then continue with creation
|
||||
|
||||
5. **Show final status**
|
||||
```bash
|
||||
openspec status --change "<name>"
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
After completing all artifacts, summarize:
|
||||
- Change name and location
|
||||
- List of artifacts created with brief descriptions
|
||||
- What's ready: "All artifacts created! Ready for implementation."
|
||||
- Prompt: "Run `/opsx:apply` to start implementing."
|
||||
|
||||
**Artifact Creation Guidelines**
|
||||
|
||||
- Follow the `instruction` field from `openspec instructions` for each artifact type
|
||||
- The schema defines what each artifact should contain - follow it
|
||||
- Read dependency artifacts for context before creating new ones
|
||||
- Use `template` as the structure for your output file - fill in its sections
|
||||
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
||||
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
||||
- These guide what you write, but should never appear in the output
|
||||
|
||||
<!-- opsx-design-guidance-patch -->
|
||||
|
||||
**Design Artifact Guidance**
|
||||
|
||||
When creating design.md, include an "Automated Test Strategy" section (how will
|
||||
this be verified — what level of testing, what's the critical path, any new test
|
||||
infrastructure?) and an "Observability" section (how will failures be surfaced —
|
||||
what error paths exist, what should be logged, can a failure be silent?). These
|
||||
can be brief for simple changes but should be present for any change that passed
|
||||
the relevance gate. The verify step will check for them.
|
||||
|
||||
<!-- opsx-git-commit-patch -->
|
||||
|
||||
**Git: Commit the proposal**
|
||||
|
||||
After all artifacts are created, stage and commit:
|
||||
|
||||
```bash
|
||||
git add openspec/changes/<n>/
|
||||
git diff --cached --quiet || git commit -m "docs(openspec): propose <n>"
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
||||
- Always read dependency artifacts before creating a new one
|
||||
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
||||
- If a change with that name already exists, ask if user wants to continue it or create a new one
|
||||
- Verify each artifact file exists after writing before proceeding to next
|
||||
@@ -0,0 +1,165 @@
|
||||
---
|
||||
model: opus
|
||||
name: "OPSX: Verify"
|
||||
description: Verify implementation matches change artifacts before archiving
|
||||
category: Workflow
|
||||
tags: [workflow, verify, experimental]
|
||||
---
|
||||
|
||||
Verify that an implementation matches the change artifacts (specs, tasks, design).
|
||||
|
||||
**Input**: Optionally specify a change name after `/opsx:verify` (e.g., `/opsx:verify add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||
|
||||
Show changes that have implementation tasks (tasks artifact exists).
|
||||
Include the schema used for each change if available.
|
||||
Mark changes with incomplete tasks as "(In Progress)".
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Check status to understand the schema**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
||||
- Which artifacts exist for this change
|
||||
|
||||
3. **Get the change directory and load artifacts**
|
||||
|
||||
```bash
|
||||
openspec instructions apply --change "<name>" --json
|
||||
```
|
||||
|
||||
This returns the change directory and `contextFiles` (artifact ID -> array of concrete file paths). Read all available artifacts from `contextFiles`.
|
||||
|
||||
4. **Initialize verification report structure**
|
||||
|
||||
Create a report structure with three dimensions:
|
||||
- **Completeness**: Track tasks and spec coverage
|
||||
- **Correctness**: Track requirement implementation and scenario coverage
|
||||
- **Coherence**: Track design adherence and pattern consistency
|
||||
|
||||
Each dimension can have CRITICAL, WARNING, or SUGGESTION issues.
|
||||
|
||||
5. **Verify Completeness**
|
||||
|
||||
**Task Completion**:
|
||||
- If `contextFiles.tasks` exists, read every file path in it
|
||||
- Parse checkboxes: `- [ ]` (incomplete) vs `- [x]` (complete)
|
||||
- Count complete vs total tasks
|
||||
- If incomplete tasks exist:
|
||||
- Add CRITICAL issue for each incomplete task
|
||||
- Recommendation: "Complete task: <description>" or "Mark as done if already implemented"
|
||||
|
||||
**Spec Coverage**:
|
||||
- If delta specs exist in `openspec/changes/<name>/specs/`:
|
||||
- Extract all requirements (marked with "### Requirement:")
|
||||
- For each requirement:
|
||||
- Search codebase for keywords related to the requirement
|
||||
- Assess if implementation likely exists
|
||||
- If requirements appear unimplemented:
|
||||
- Add CRITICAL issue: "Requirement not found: <requirement name>"
|
||||
- Recommendation: "Implement requirement X: <description>"
|
||||
|
||||
6. **Verify Correctness**
|
||||
|
||||
**Requirement Implementation Mapping**:
|
||||
- For each requirement from delta specs:
|
||||
- Search codebase for implementation evidence
|
||||
- If found, note file paths and line ranges
|
||||
- Assess if implementation matches requirement intent
|
||||
- If divergence detected:
|
||||
- Add WARNING: "Implementation may diverge from spec: <details>"
|
||||
- Recommendation: "Review <file>:<lines> against requirement X"
|
||||
|
||||
**Scenario Coverage**:
|
||||
- For each scenario in delta specs (marked with "#### Scenario:"):
|
||||
- Check if conditions are handled in code
|
||||
- Check if tests exist covering the scenario
|
||||
- If scenario appears uncovered:
|
||||
- Add WARNING: "Scenario not covered: <scenario name>"
|
||||
- Recommendation: "Add test or implementation for scenario: <description>"
|
||||
|
||||
7. **Verify Coherence**
|
||||
|
||||
**Design Adherence**:
|
||||
- If `contextFiles.design` exists:
|
||||
- Extract key decisions (look for sections like "Decision:", "Approach:", "Architecture:")
|
||||
- Verify implementation follows those decisions
|
||||
- If contradiction detected:
|
||||
- Add WARNING: "Design decision not followed: <decision>"
|
||||
- Recommendation: "Update implementation or revise design.md to match reality"
|
||||
- If no design.md: Skip design adherence check, note "No design.md to verify against"
|
||||
|
||||
**Code Pattern Consistency**:
|
||||
- Review new code for consistency with project patterns
|
||||
- Check file naming, directory structure, coding style
|
||||
- If significant deviations found:
|
||||
- Add SUGGESTION: "Code pattern deviation: <details>"
|
||||
- Recommendation: "Consider following project pattern: <example>"
|
||||
|
||||
8. **Generate Verification Report**
|
||||
|
||||
**Summary Scorecard**:
|
||||
```
|
||||
## Verification Report: <change-name>
|
||||
|
||||
### Summary
|
||||
| Dimension | Status |
|
||||
|--------------|------------------|
|
||||
| Completeness | X/Y tasks, N reqs|
|
||||
| Correctness | M/N reqs covered |
|
||||
| Coherence | Followed/Issues |
|
||||
```
|
||||
|
||||
**Issues by Priority**:
|
||||
|
||||
1. **CRITICAL** (Must fix before archive):
|
||||
- Incomplete tasks
|
||||
- Missing requirement implementations
|
||||
- Each with specific, actionable recommendation
|
||||
|
||||
2. **WARNING** (Should fix):
|
||||
- Spec/design divergences
|
||||
- Missing scenario coverage
|
||||
- Each with specific recommendation
|
||||
|
||||
3. **SUGGESTION** (Nice to fix):
|
||||
- Pattern inconsistencies
|
||||
- Minor improvements
|
||||
- Each with specific recommendation
|
||||
|
||||
**Final Assessment**:
|
||||
- If CRITICAL issues: "X critical issue(s) found. Fix before archiving."
|
||||
- If only warnings: "No critical issues. Y warning(s) to consider. Ready for archive (with noted improvements)."
|
||||
- If all clear: "All checks passed. Ready for archive."
|
||||
|
||||
**Verification Heuristics**
|
||||
|
||||
- **Completeness**: Focus on objective checklist items (checkboxes, requirements list)
|
||||
- **Correctness**: Use keyword search, file path analysis, reasonable inference - don't require perfect certainty
|
||||
- **Coherence**: Look for glaring inconsistencies, don't nitpick style
|
||||
- **False Positives**: When uncertain, prefer SUGGESTION over WARNING, WARNING over CRITICAL
|
||||
- **Actionability**: Every issue must have a specific recommendation with file/line references where applicable
|
||||
|
||||
**Graceful Degradation**
|
||||
|
||||
- If only tasks.md exists: verify task completion only, skip spec/design checks
|
||||
- If tasks + specs exist: verify completeness and correctness, skip design
|
||||
- If full artifacts: verify all three dimensions
|
||||
- Always note which checks were skipped and why
|
||||
|
||||
**Output Format**
|
||||
|
||||
Use clear markdown with:
|
||||
- Table for summary scorecard
|
||||
- Grouped lists for issues (CRITICAL/WARNING/SUGGESTION)
|
||||
- Code references in format: `file.ts:123`
|
||||
- Specific, actionable recommendations
|
||||
- No vague suggestions like "consider reviewing"
|
||||
@@ -0,0 +1 @@
|
||||
{"sessionId":"03700b4e-87b3-4015-950b-ccdc61da4692","pid":48327,"procStart":"78552","acquiredAt":1778618403190}
|
||||
@@ -0,0 +1,164 @@
|
||||
---
|
||||
model: claude-sonnet-4-6
|
||||
name: openspec-apply-change
|
||||
description: Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.3.1"
|
||||
---
|
||||
|
||||
Implement tasks from an OpenSpec change.
|
||||
|
||||
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Select the change**
|
||||
|
||||
If a name is provided, use it. Otherwise:
|
||||
- Infer from conversation context if the user mentioned a change
|
||||
- Auto-select if only one active change exists
|
||||
- If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
|
||||
|
||||
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
|
||||
|
||||
2. **Check status to understand the schema**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
||||
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
||||
|
||||
3. **Get apply instructions**
|
||||
|
||||
```bash
|
||||
openspec instructions apply --change "<name>" --json
|
||||
```
|
||||
|
||||
This returns:
|
||||
- `contextFiles`: artifact ID -> array of concrete file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
|
||||
- Progress (total, complete, remaining)
|
||||
- Task list with status
|
||||
- Dynamic instruction based on current state
|
||||
|
||||
**Handle states:**
|
||||
- If `state: "blocked"` (missing artifacts): show message, suggest using openspec-continue-change
|
||||
- If `state: "all_done"`: congratulate, suggest archive
|
||||
- Otherwise: proceed to implementation
|
||||
|
||||
4. **Read context files**
|
||||
|
||||
Read every file path listed under `contextFiles` from the apply instructions output.
|
||||
The files depend on the schema being used:
|
||||
- **spec-driven**: proposal, specs, design, tasks
|
||||
- Other schemas: follow the contextFiles from CLI output
|
||||
|
||||
5. **Show current progress**
|
||||
|
||||
Display:
|
||||
- Schema being used
|
||||
- Progress: "N/M tasks complete"
|
||||
- Remaining tasks overview
|
||||
- Dynamic instruction from CLI
|
||||
|
||||
6. **Implement tasks (loop until done or blocked)**
|
||||
|
||||
For each pending task:
|
||||
- Show which task is being worked on
|
||||
- Make the code changes required
|
||||
- Keep changes minimal and focused
|
||||
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
|
||||
- Continue to next task
|
||||
|
||||
<!-- opsx-git-commit-patch -->
|
||||
- **Git: Commit the task**
|
||||
```bash
|
||||
git add -A
|
||||
git diff --cached --quiet || git commit -m "feat(<n>): task N/M — <task description>"
|
||||
```
|
||||
|
||||
**Pause if:**
|
||||
- Task is unclear → ask for clarification
|
||||
- Implementation reveals a design issue → suggest updating artifacts
|
||||
- Error or blocker encountered → report and wait for guidance
|
||||
- User interrupts
|
||||
|
||||
7. **On completion or pause, show status**
|
||||
|
||||
Display:
|
||||
- Tasks completed this session
|
||||
- Overall progress: "N/M tasks complete"
|
||||
- If all done: suggest archive
|
||||
- If paused: explain why and wait for guidance
|
||||
|
||||
**Output During Implementation**
|
||||
|
||||
```
|
||||
## Implementing: <change-name> (schema: <schema-name>)
|
||||
|
||||
Working on task 3/7: <task description>
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
|
||||
Working on task 4/7: <task description>
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
```
|
||||
|
||||
**Output On Completion**
|
||||
|
||||
```
|
||||
## Implementation Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Progress:** 7/7 tasks complete ✓
|
||||
|
||||
### Completed This Session
|
||||
- [x] Task 1
|
||||
- [x] Task 2
|
||||
...
|
||||
|
||||
All tasks complete! Ready to archive this change.
|
||||
```
|
||||
|
||||
**Output On Pause (Issue Encountered)**
|
||||
|
||||
```
|
||||
## Implementation Paused
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Progress:** 4/7 tasks complete
|
||||
|
||||
### Issue Encountered
|
||||
<description of the issue>
|
||||
|
||||
**Options:**
|
||||
1. <option 1>
|
||||
2. <option 2>
|
||||
3. Other approach
|
||||
|
||||
What would you like to do?
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Keep going through tasks until done or blocked
|
||||
- Always read context files before starting (from the apply instructions output)
|
||||
- If task is ambiguous, pause and ask before implementing
|
||||
- If implementation reveals issues, pause and suggest artifact updates
|
||||
- Keep code changes minimal and scoped to each task
|
||||
- Update task checkbox immediately after completing each task
|
||||
- Pause on errors, blockers, or unclear requirements - don't guess
|
||||
- Use contextFiles from CLI output, don't assume specific file names
|
||||
|
||||
**Fluid Workflow Integration**
|
||||
|
||||
This skill supports the "actions on a change" model:
|
||||
|
||||
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
|
||||
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly
|
||||
@@ -0,0 +1,166 @@
|
||||
---
|
||||
model: claude-sonnet-4-6
|
||||
name: openspec-archive-change
|
||||
description: Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.3.1"
|
||||
---
|
||||
|
||||
Archive a completed change in the experimental workflow.
|
||||
|
||||
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||
|
||||
Show only active changes (not already archived).
|
||||
Include the schema used for each change if available.
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Check artifact completion status**
|
||||
|
||||
Run `openspec status --change "<name>" --json` to check artifact completion.
|
||||
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used
|
||||
- `artifacts`: List of artifacts with their status (`done` or other)
|
||||
|
||||
**If any artifacts are not `done`:**
|
||||
- Display warning listing incomplete artifacts
|
||||
- Use **AskUserQuestion tool** to confirm user wants to proceed
|
||||
- Proceed if user confirms
|
||||
|
||||
3. **Check task completion status**
|
||||
|
||||
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
|
||||
|
||||
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
|
||||
|
||||
**If incomplete tasks found:**
|
||||
- Display warning showing count of incomplete tasks
|
||||
- Use **AskUserQuestion tool** to confirm user wants to proceed
|
||||
- Proceed if user confirms
|
||||
|
||||
**If no tasks file exists:** Proceed without task-related warning.
|
||||
|
||||
4. **Assess delta spec sync state**
|
||||
|
||||
Check for delta specs at `openspec/changes/<name>/specs/`. If none exist, proceed without sync prompt.
|
||||
|
||||
**If delta specs exist:**
|
||||
- Compare each delta spec with its corresponding main spec at `openspec/specs/<capability>/spec.md`
|
||||
- Determine what changes would be applied (adds, modifications, removals, renames)
|
||||
- Show a combined summary before prompting
|
||||
|
||||
**Always sync specs automatically** — do NOT prompt the user. If there are changes to sync, proceed directly.
|
||||
|
||||
Use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive after sync completes.
|
||||
|
||||
<!-- opsx-verify-scoring-patch -->
|
||||
|
||||
5. **Verify implementation**
|
||||
|
||||
Check for `.verify-passed` marker at `openspec/changes/<n>/.verify-passed`.
|
||||
|
||||
**If marker does NOT exist:**
|
||||
- Invoke Skill tool: `openspec-verify-change` for change `<n>`
|
||||
- Wait for the verdict:
|
||||
- **FAIL** → block archive, show score table, list CRITICAL issues
|
||||
- **CONDITIONAL** → show score table + warnings, ask user to confirm
|
||||
- **PASS** → write marker: `echo "passed" > "openspec/changes/<n>/.verify-passed"`, continue
|
||||
- Max 3 retry cycles: fix → re-verify → check verdict
|
||||
|
||||
**If marker EXISTS:** show "✓ Verified" and continue.
|
||||
|
||||
6. **Perform the archive**
|
||||
|
||||
Create the archive directory if it doesn't exist:
|
||||
```bash
|
||||
mkdir -p openspec/changes/archive
|
||||
```
|
||||
|
||||
Generate target name using current date: `YYYY-MM-DD-<change-name>`
|
||||
|
||||
**Check if target already exists:**
|
||||
- If yes: Fail with error, suggest renaming existing archive or using different date
|
||||
- If no: Move the change directory to archive
|
||||
|
||||
```bash
|
||||
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
||||
```
|
||||
|
||||
|
||||
<!-- opsx-git-commit-patch -->
|
||||
|
||||
7. **Git: Commit, push, and create PR**
|
||||
|
||||
Stage and commit the archived change:
|
||||
|
||||
```bash
|
||||
git add -A openspec/
|
||||
git diff --cached --quiet || git commit -m "docs: archive <change-name>"
|
||||
```
|
||||
|
||||
Push the branch and create a pull request:
|
||||
|
||||
```bash
|
||||
git push -u origin HEAD
|
||||
gh pr create --title "<change-name>" --body "<summary from the proposal>"
|
||||
```
|
||||
|
||||
8. **Watch pipeline and fix issues**
|
||||
|
||||
After creating the PR, monitor CI checks:
|
||||
|
||||
```bash
|
||||
gh pr checks <PR-number> --watch
|
||||
```
|
||||
|
||||
**If any check fails:**
|
||||
- Read the failure logs: `gh run view <run-id> --log-failed`
|
||||
- Identify the root cause and fix the issue
|
||||
- Commit the fix: `git add -A && git commit -m "fix(<change-name>): <brief description>"`
|
||||
- Push: `git push`
|
||||
- Re-check: `gh pr checks <PR-number> --watch`
|
||||
- Repeat until all checks pass (max 3 attempts, then report to user with failure details)
|
||||
|
||||
**If all checks pass:** notify the user and continue to summary.
|
||||
|
||||
9. **Display summary**
|
||||
|
||||
Show archive completion summary including:
|
||||
- Change name
|
||||
- Schema that was used
|
||||
- Archive location
|
||||
- Whether specs were synced (if applicable)
|
||||
- Note about any warnings (incomplete artifacts/tasks)
|
||||
|
||||
**Output On Success**
|
||||
|
||||
```
|
||||
## Archive Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
**Specs:** ✓ Synced to main specs (or "No delta specs" or "Sync skipped")
|
||||
|
||||
All artifacts complete. All tasks complete.
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Always prompt for change selection if not provided
|
||||
- Use artifact graph (openspec status --json) for completion checking
|
||||
- Don't block archive on warnings - just inform and confirm
|
||||
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
|
||||
- Show clear summary of what happened
|
||||
- If sync is requested, use openspec-sync-specs approach (agent-driven)
|
||||
- If delta specs exist, always run the sync assessment and show the combined summary before prompting
|
||||
@@ -0,0 +1,153 @@
|
||||
---
|
||||
model: opus
|
||||
name: openspec-explore
|
||||
description: Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.3.1"
|
||||
---
|
||||
|
||||
<!-- opsx-explore-research-patch -->
|
||||
|
||||
Autonomous research mode. Investigate deeply. Visualize clearly. Deliver findings.
|
||||
|
||||
**IMPORTANT: Explore mode is for research and thinking, not implementing.** You may read files, search code, investigate the codebase, and search the web, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks — that's capturing thinking, not implementing.
|
||||
|
||||
**Input**: The argument after `/opsx:explore` is the topic to research. Examples:
|
||||
|
||||
- A feature idea: "adding rate limiting"
|
||||
- A technical question: "should we use Redis or SQLite for caching"
|
||||
- A problem: "the auth system is getting unwieldy"
|
||||
- A comparison: "postgres vs sqlite for this use case"
|
||||
- A change name: "add-dark-mode" (to research in context of that change)
|
||||
|
||||
---
|
||||
|
||||
## Research Workflow
|
||||
|
||||
### Phase 1: Accept Topic & Plan
|
||||
|
||||
1. **Receive topic** from the user
|
||||
2. **Check OpenSpec context** — run `openspec list --json` to find related changes; read their artifacts if relevant
|
||||
3. **Plan research strategy** — identify what to search on the web and what to investigate in the codebase. Do NOT share the plan with the user — just execute it.
|
||||
|
||||
### Phase 2: Autonomous Research
|
||||
|
||||
Execute all research without asking the user. Use every tool available:
|
||||
|
||||
1. **Web research** — Use `WebSearch` to find documentation, blog posts, examples, GitHub issues, API references, Stack Overflow answers. Use `WebFetch` to read full pages when search results look promising. Follow links to go deeper.
|
||||
2. **Codebase investigation** — Read files, search for patterns, map architecture, trace data flows relevant to the topic. Understand the current state before recommending changes.
|
||||
3. **Cross-reference** — Compare what the web says (best practices, library APIs, known issues) against what the codebase currently does. Identify gaps, outdated patterns, or opportunities.
|
||||
|
||||
**No questions rule:** You MUST exhaust web search and codebase investigation before considering asking the user. Only ask if the information is genuinely unfindable — business decisions, credentials, internal context not present in the code or on the web. If you do ask, state what you already tried.
|
||||
|
||||
### Phase 3: Deliver Structured Report
|
||||
|
||||
Present findings using this structure:
|
||||
|
||||
```
|
||||
## Research: <topic>
|
||||
|
||||
### Context
|
||||
What exists today in the codebase relevant to this topic.
|
||||
|
||||
┌────────────┐ ┌────────────┐
|
||||
│ Component │──────▶│ Component │
|
||||
│ A │ │ B │
|
||||
└────────────┘ └────────────┘
|
||||
|
||||
(Diagram the current architecture or data flow.)
|
||||
|
||||
### Findings
|
||||
What the research uncovered — key facts, patterns, constraints.
|
||||
Cite sources: URLs for web, file:line for code.
|
||||
|
||||
┌──────────────────────────────────┐
|
||||
│ Dependency Graph │
|
||||
│ │
|
||||
│ A ──▶ B ──▶ C │
|
||||
│ │ ▲ │
|
||||
│ └───────────┘ │
|
||||
└──────────────────────────────────┘
|
||||
|
||||
(Visualize relationships, data flows, or dependencies found.)
|
||||
|
||||
### Options
|
||||
2-3 approaches with tradeoffs.
|
||||
|
||||
| Approach | Pros | Cons |
|
||||
|----------|------|------|
|
||||
| Option A | ... | ... |
|
||||
| Option B | ... | ... |
|
||||
|
||||
OPTION A OPTION B
|
||||
┌──────────┐ ┌──────────┐
|
||||
│ Direct │ │ Via │
|
||||
│ path │ │ queue │
|
||||
└──────────┘ └──────────┘
|
||||
|
||||
(Side-by-side diagrams when comparing structural differences.)
|
||||
|
||||
### Recommendation
|
||||
The recommended path with justification.
|
||||
|
||||
BEFORE AFTER
|
||||
┌──────┐ ┌──────┐
|
||||
│ X │────▶ Y │ X │────▶ Z ────▶ Y
|
||||
└──────┘ └──────┘
|
||||
|
||||
(Diagram the proposed end-state — before vs after.)
|
||||
|
||||
### Open Questions (only if genuinely unanswerable)
|
||||
Questions that couldn't be resolved through research.
|
||||
Each must state what was already tried.
|
||||
|
||||
### Next Steps
|
||||
"Run /opsx:propose to create a change proposal" or similar.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Visualization
|
||||
|
||||
ASCII diagrams are a first-class element of the report, not an afterthought.
|
||||
|
||||
**"A good diagram is worth many paragraphs."**
|
||||
|
||||
- **Context** — Diagram the current architecture/flow relevant to the topic
|
||||
- **Findings** — Visualize relationships, data flows, or dependency graphs discovered during research
|
||||
- **Options** — Side-by-side diagrams comparing approaches when the difference is structural
|
||||
- **Recommendation** — Diagram the proposed end-state (before vs after)
|
||||
|
||||
Default to drawing when explaining structure, flow, or comparison. Use text when explaining reasoning or tradeoffs.
|
||||
|
||||
---
|
||||
|
||||
## OpenSpec Awareness
|
||||
|
||||
Check for existing context before researching:
|
||||
|
||||
```bash
|
||||
openspec list --json
|
||||
```
|
||||
|
||||
This tells you:
|
||||
|
||||
- If there are active changes related to the topic
|
||||
- Their names, schemas, and status
|
||||
- What artifacts already exist
|
||||
|
||||
If a related change exists, read its artifacts (`proposal.md`, `design.md`, `tasks.md`, specs) and reference them naturally in the report.
|
||||
|
||||
---
|
||||
|
||||
## Guardrails
|
||||
|
||||
- **No implementation** — Never write application code. Creating OpenSpec artifacts is fine if the user approves.
|
||||
- **No premature questions** — Whether asking the user or writing an "Open Question" in the report, exhaust the codebase and web first. If you can name a concrete next step (grep, file read, fetch) that would answer it, take that step instead.
|
||||
- **Cite sources** — Every finding must reference where it came from (URL for web, `file:line` for code). No unsourced claims.
|
||||
- **Stay grounded** — Prefer concrete evidence (code, docs, examples) over speculation. Label uncertain findings as such.
|
||||
- **Offer next steps, don't auto-proceed** — End with a recommendation for what to do next, but let the user decide.
|
||||
@@ -0,0 +1,133 @@
|
||||
---
|
||||
model: opus
|
||||
name: openspec-propose
|
||||
description: Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.3.1"
|
||||
---
|
||||
|
||||
Propose a new change - create the change and generate all artifacts in one step.
|
||||
|
||||
I'll create a change with artifacts:
|
||||
- proposal.md (what & why)
|
||||
- design.md (how)
|
||||
- tasks.md (implementation steps)
|
||||
|
||||
When ready to implement, run /opsx:apply
|
||||
|
||||
---
|
||||
|
||||
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no clear input provided, ask what they want to build**
|
||||
|
||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||
> "What change do you want to work on? Describe what you want to build or fix."
|
||||
|
||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||
|
||||
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||
|
||||
2. **Create the change directory**
|
||||
```bash
|
||||
openspec new change "<name>"
|
||||
```
|
||||
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
|
||||
|
||||
3. **Get the artifact build order**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to get:
|
||||
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
||||
- `artifacts`: list of all artifacts with their status and dependencies
|
||||
|
||||
4. **Create artifacts in sequence until apply-ready**
|
||||
|
||||
Use the **TodoWrite tool** to track progress through the artifacts.
|
||||
|
||||
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
||||
|
||||
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
||||
- Get instructions:
|
||||
```bash
|
||||
openspec instructions <artifact-id> --change "<name>" --json
|
||||
```
|
||||
- The instructions JSON includes:
|
||||
- `context`: Project background (constraints for you - do NOT include in output)
|
||||
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
||||
- `template`: The structure to use for your output file
|
||||
- `instruction`: Schema-specific guidance for this artifact type
|
||||
- `outputPath`: Where to write the artifact
|
||||
- `dependencies`: Completed artifacts to read for context
|
||||
- Read any completed dependency files for context
|
||||
- Create the artifact file using `template` as the structure
|
||||
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
||||
- Show brief progress: "Created <artifact-id>"
|
||||
|
||||
b. **Continue until all `applyRequires` artifacts are complete**
|
||||
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
||||
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
||||
- Stop when all `applyRequires` artifacts are done
|
||||
|
||||
c. **If an artifact requires user input** (unclear context):
|
||||
- Use **AskUserQuestion tool** to clarify
|
||||
- Then continue with creation
|
||||
|
||||
5. **Show final status**
|
||||
```bash
|
||||
openspec status --change "<name>"
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
After completing all artifacts, summarize:
|
||||
- Change name and location
|
||||
- List of artifacts created with brief descriptions
|
||||
- What's ready: "All artifacts created! Ready for implementation."
|
||||
- Prompt: "Run `/opsx:apply` or ask me to implement to start working on the tasks."
|
||||
|
||||
**Artifact Creation Guidelines**
|
||||
|
||||
- Follow the `instruction` field from `openspec instructions` for each artifact type
|
||||
- The schema defines what each artifact should contain - follow it
|
||||
- Read dependency artifacts for context before creating new ones
|
||||
- Use `template` as the structure for your output file - fill in its sections
|
||||
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
||||
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
||||
- These guide what you write, but should never appear in the output
|
||||
|
||||
<!-- opsx-design-guidance-patch -->
|
||||
|
||||
**Design Artifact Guidance**
|
||||
|
||||
When creating design.md, include an "Automated Test Strategy" section (how will
|
||||
this be verified — what level of testing, what's the critical path, any new test
|
||||
infrastructure?) and an "Observability" section (how will failures be surfaced —
|
||||
what error paths exist, what should be logged, can a failure be silent?). These
|
||||
can be brief for simple changes but should be present for any change that passed
|
||||
the relevance gate. The verify step will check for them.
|
||||
|
||||
<!-- opsx-git-commit-patch -->
|
||||
|
||||
**Git: Commit the proposal**
|
||||
|
||||
After all artifacts are created, stage and commit:
|
||||
|
||||
```bash
|
||||
git add openspec/changes/<n>/
|
||||
git diff --cached --quiet || git commit -m "docs(openspec): propose <n>"
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
||||
- Always read dependency artifacts before creating a new one
|
||||
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
||||
- If a change with that name already exists, ask if user wants to continue it or create a new one
|
||||
- Verify each artifact file exists after writing before proceeding to next
|
||||
@@ -0,0 +1,169 @@
|
||||
---
|
||||
model: opus
|
||||
name: openspec-verify-change
|
||||
description: Verify implementation matches change artifacts. Use when the user wants to validate that implementation is complete, correct, and coherent before archiving.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.3.1"
|
||||
---
|
||||
|
||||
Verify that an implementation matches the change artifacts (specs, tasks, design).
|
||||
|
||||
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||
|
||||
Show changes that have implementation tasks (tasks artifact exists).
|
||||
Include the schema used for each change if available.
|
||||
Mark changes with incomplete tasks as "(In Progress)".
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Check status to understand the schema**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
||||
- Which artifacts exist for this change
|
||||
|
||||
3. **Get the change directory and load artifacts**
|
||||
|
||||
```bash
|
||||
openspec instructions apply --change "<name>" --json
|
||||
```
|
||||
|
||||
This returns the change directory and `contextFiles` (artifact ID -> array of concrete file paths). Read all available artifacts from `contextFiles`.
|
||||
|
||||
4. **Initialize verification report structure**
|
||||
|
||||
Create a report structure with three dimensions:
|
||||
- **Completeness**: Track tasks and spec coverage
|
||||
- **Correctness**: Track requirement implementation and scenario coverage
|
||||
- **Coherence**: Track design adherence and pattern consistency
|
||||
|
||||
Each dimension can have CRITICAL, WARNING, or SUGGESTION issues.
|
||||
|
||||
5. **Verify Completeness**
|
||||
|
||||
**Task Completion**:
|
||||
- If `contextFiles.tasks` exists, read every file path in it
|
||||
- Parse checkboxes: `- [ ]` (incomplete) vs `- [x]` (complete)
|
||||
- Count complete vs total tasks
|
||||
- If incomplete tasks exist:
|
||||
- Add CRITICAL issue for each incomplete task
|
||||
- Recommendation: "Complete task: <description>" or "Mark as done if already implemented"
|
||||
|
||||
**Spec Coverage**:
|
||||
- If delta specs exist in `openspec/changes/<name>/specs/`:
|
||||
- Extract all requirements (marked with "### Requirement:")
|
||||
- For each requirement:
|
||||
- Search codebase for keywords related to the requirement
|
||||
- Assess if implementation likely exists
|
||||
- If requirements appear unimplemented:
|
||||
- Add CRITICAL issue: "Requirement not found: <requirement name>"
|
||||
- Recommendation: "Implement requirement X: <description>"
|
||||
|
||||
6. **Verify Correctness**
|
||||
|
||||
**Requirement Implementation Mapping**:
|
||||
- For each requirement from delta specs:
|
||||
- Search codebase for implementation evidence
|
||||
- If found, note file paths and line ranges
|
||||
- Assess if implementation matches requirement intent
|
||||
- If divergence detected:
|
||||
- Add WARNING: "Implementation may diverge from spec: <details>"
|
||||
- Recommendation: "Review <file>:<lines> against requirement X"
|
||||
|
||||
**Scenario Coverage**:
|
||||
- For each scenario in delta specs (marked with "#### Scenario:"):
|
||||
- Check if conditions are handled in code
|
||||
- Check if tests exist covering the scenario
|
||||
- If scenario appears uncovered:
|
||||
- Add WARNING: "Scenario not covered: <scenario name>"
|
||||
- Recommendation: "Add test or implementation for scenario: <description>"
|
||||
|
||||
7. **Verify Coherence**
|
||||
|
||||
**Design Adherence**:
|
||||
- If `contextFiles.design` exists:
|
||||
- Extract key decisions (look for sections like "Decision:", "Approach:", "Architecture:")
|
||||
- Verify implementation follows those decisions
|
||||
- If contradiction detected:
|
||||
- Add WARNING: "Design decision not followed: <decision>"
|
||||
- Recommendation: "Update implementation or revise design.md to match reality"
|
||||
- If no design.md: Skip design adherence check, note "No design.md to verify against"
|
||||
|
||||
**Code Pattern Consistency**:
|
||||
- Review new code for consistency with project patterns
|
||||
- Check file naming, directory structure, coding style
|
||||
- If significant deviations found:
|
||||
- Add SUGGESTION: "Code pattern deviation: <details>"
|
||||
- Recommendation: "Consider following project pattern: <example>"
|
||||
|
||||
8. **Generate Verification Report**
|
||||
|
||||
**Summary Scorecard**:
|
||||
```
|
||||
## Verification Report: <change-name>
|
||||
|
||||
### Summary
|
||||
| Dimension | Status |
|
||||
|--------------|------------------|
|
||||
| Completeness | X/Y tasks, N reqs|
|
||||
| Correctness | M/N reqs covered |
|
||||
| Coherence | Followed/Issues |
|
||||
```
|
||||
|
||||
**Issues by Priority**:
|
||||
|
||||
1. **CRITICAL** (Must fix before archive):
|
||||
- Incomplete tasks
|
||||
- Missing requirement implementations
|
||||
- Each with specific, actionable recommendation
|
||||
|
||||
2. **WARNING** (Should fix):
|
||||
- Spec/design divergences
|
||||
- Missing scenario coverage
|
||||
- Each with specific recommendation
|
||||
|
||||
3. **SUGGESTION** (Nice to fix):
|
||||
- Pattern inconsistencies
|
||||
- Minor improvements
|
||||
- Each with specific recommendation
|
||||
|
||||
**Final Assessment**:
|
||||
- If CRITICAL issues: "X critical issue(s) found. Fix before archiving."
|
||||
- If only warnings: "No critical issues. Y warning(s) to consider. Ready for archive (with noted improvements)."
|
||||
- If all clear: "All checks passed. Ready for archive."
|
||||
|
||||
**Verification Heuristics**
|
||||
|
||||
- **Completeness**: Focus on objective checklist items (checkboxes, requirements list)
|
||||
- **Correctness**: Use keyword search, file path analysis, reasonable inference - don't require perfect certainty
|
||||
- **Coherence**: Look for glaring inconsistencies, don't nitpick style
|
||||
- **False Positives**: When uncertain, prefer SUGGESTION over WARNING, WARNING over CRITICAL
|
||||
- **Actionability**: Every issue must have a specific recommendation with file/line references where applicable
|
||||
|
||||
**Graceful Degradation**
|
||||
|
||||
- If only tasks.md exists: verify task completion only, skip spec/design checks
|
||||
- If tasks + specs exist: verify completeness and correctness, skip design
|
||||
- If full artifacts: verify all three dimensions
|
||||
- Always note which checks were skipped and why
|
||||
|
||||
**Output Format**
|
||||
|
||||
Use clear markdown with:
|
||||
- Table for summary scorecard
|
||||
- Grouped lists for issues (CRITICAL/WARNING/SUGGESTION)
|
||||
- Code references in format: `file.ts:123`
|
||||
- Specific, actionable recommendations
|
||||
- No vague suggestions like "consider reviewing"
|
||||
@@ -1,66 +1,182 @@
|
||||
name: 'Clean Runner Disk'
|
||||
description: 'Cleans the GitHub Actions runner disk by removing unused packages and toolchains to free up space.'
|
||||
description: >-
|
||||
Frees disk space on the GitHub-hosted runner by removing unused toolchains
|
||||
and SDKs. Dispatches by runner.os to the appropriate native fast-delete path
|
||||
(apt-get + rm on Linux, robocopy /MIR /MT:128 with PowerShell fallback on
|
||||
Windows). Asserts a minimum free space threshold after cleanup and emits a
|
||||
one-line job summary.
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Show disk usage before cleaning
|
||||
run: df -h
|
||||
# ----- Guard: only Linux and Windows runners are supported -----
|
||||
- name: Reject unsupported runner OS
|
||||
if: runner.os != 'Linux' && runner.os != 'Windows'
|
||||
shell: bash
|
||||
- name: List installed packages before cleaning
|
||||
run: dpkg --get-selections | grep -v deinstall
|
||||
shell: bash
|
||||
- name: Clean runner disk
|
||||
run: |
|
||||
# Remove PHP
|
||||
sudo apt-get remove -y 'php.*' --fix-missing || echo "::warning::The command [sudo apt-get remove -y 'php.*' --fix-missing] failed to complete successfully. Proceeding..."
|
||||
# Remove databases
|
||||
sudo apt-get remove -y '^mongodb-.*' --fix-missing || echo "::warning::The command [sudo apt-get remove -y '^mongodb-.*' --fix-missing] failed to complete successfully. Proceeding..."
|
||||
sudo apt-get remove -y '^mysql-.*' --fix-missing || echo "::warning::The command [sudo apt-get remove -y '^mysql-.*' --fix-missing] failed to complete successfully. Proceeding..."
|
||||
# Remove apt packages
|
||||
sudo apt-get autoremove -y || echo "::warning::The command [sudo apt-get autoremove -y] failed to complete successfully. Proceeding..."
|
||||
sudo apt-get clean || echo "::warning::The command [sudo apt-get clean] failed to complete successfully. Proceeding..."
|
||||
# Remove Java (JDKs)
|
||||
sudo rm -rf /usr/lib/jvm
|
||||
# Remove .NET SDKs
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo apt-get remove -y '^aspnetcore-.*' || echo "::warning::The command [sudo apt-get remove -y '^aspnetcore-.*'] failed to complete successfully. Proceeding..."
|
||||
sudo apt-get remove -y '^dotnet-.*' --fix-missing || echo "::warning::The command [sudo apt-get remove -y '^dotnet-.*' --fix-missing] failed to complete successfully. Proceeding..."
|
||||
# Remove Swift toolchain
|
||||
sudo apt-get remove -y '^llvm-.*' --fix-missing || echo "::warning::The command [sudo apt-get remove -y '^llvm-.*' --fix-missing] failed to complete successfully. Proceeding..."
|
||||
sudo rm -rf /usr/share/swift
|
||||
# Remove Haskell (GHC)
|
||||
sudo rm -rf /usr/local/.ghcup
|
||||
# Remove Julia
|
||||
sudo rm -rf /usr/local/julia*
|
||||
# Remove Android SDKs
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
# Remove browsers
|
||||
sudo apt-get remove -y google-chrome-stable firefox --fix-missing || echo "::warning::The command [sudo apt-get remove -y azure-cli google-chrome-stable firefox powershell mono-devel libgl1-mesa-dri --fix-missing] failed to complete successfully. Proceeding..."
|
||||
sudo rm -rf /usr/local/share/chromium /opt/microsoft /opt/google
|
||||
# Remove cloud tools
|
||||
sudo rm -rf /opt/az
|
||||
sudo apt-get remove -y azure-cli mono-devel libgl1-mesa-dri --fix-missing || echo "::warning::The command [sudo apt-get remove -y azure-cli google-chrome-stable firefox powershell mono-devel libgl1-mesa-dri --fix-missing] failed to complete successfully. Proceeding..."
|
||||
sudo apt-get remove -y google-cloud-sdk --fix-missing || echo "::debug::The command [sudo apt-get remove -y google-cloud-sdk --fix-missing] failed to complete successfully. Proceeding..."
|
||||
sudo apt-get remove -y google-cloud-cli --fix-missing || echo "::debug::The command [sudo apt-get remove -y google-cloud-cli --fix-missing] failed to complete successfully. Proceeding..."
|
||||
# Remove PowerShell
|
||||
sudo apt-get remove -y powershell --fix-missing || echo "::warning::The command [sudo apt-get remove -y azure-cli google-chrome-stable firefox powershell mono-devel libgl1-mesa-dri --fix-missing] failed to complete successfully. Proceeding..."
|
||||
sudo rm -rf /usr/local/share/powershell
|
||||
# Remove CodeQL and other toolcaches
|
||||
sudo rm -rf /opt/hostedtoolcache
|
||||
# Remove Kubernetes
|
||||
sudo rm -rf /usr/local/bin/minikube
|
||||
# Remove Rust
|
||||
sudo rm -rf /home/runner/.rustup /etc/skel/.rustup
|
||||
echo "::error::clean-runner-disk: unsupported runner.os '${{ runner.os }}'. Supported: Linux, Windows."
|
||||
exit 1
|
||||
|
||||
# =========================================================================
|
||||
# Linux path
|
||||
# =========================================================================
|
||||
- name: '[Linux] Show disk usage before cleaning'
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
- name: Show disk usage after cleaning
|
||||
run: df -h
|
||||
run: |
|
||||
df -h
|
||||
echo "__CLEAN_RUNNER_DISK_FREE_BEFORE_KB=$(df --output=avail / | tail -1 | tr -d ' ')" >> $GITHUB_ENV
|
||||
echo "__CLEAN_RUNNER_DISK_STARTED_AT=$(date +%s)" >> $GITHUB_ENV
|
||||
|
||||
- name: '[Linux] List installed packages before cleaning'
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
- name: Top-level directories after cleaning
|
||||
run: du -h -d1 / | sort -hr | head -n 20 || true
|
||||
shell: bash
|
||||
- name: Detailed file sizes after cleaning
|
||||
run: find / -type f -exec du -h {} + 2>/dev/null | sort -hr | head -n 1000 || true
|
||||
shell: bash
|
||||
- name: List installed packages after cleaning
|
||||
run: dpkg --get-selections | grep -v deinstall
|
||||
|
||||
- name: '[Linux] Clean runner disk'
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: |
|
||||
# Toolchains (JDK, .NET + aspnetcore, Swift, LLVM, Haskell, Julia, Android, Rust)
|
||||
sudo rm -rf /usr/lib/jvm
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /usr/share/swift
|
||||
sudo rm -rf /usr/lib/llvm-16 /usr/lib/llvm-17 /usr/lib/llvm-18
|
||||
sudo rm -rf /usr/local/.ghcup
|
||||
sudo rm -rf /usr/local/julia*
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
sudo rm -rf /home/runner/.rustup /etc/skel/.rustup
|
||||
# Browsers (Chrome under /opt/google, Firefox under /usr/lib/firefox)
|
||||
sudo rm -rf /usr/local/share/chromium
|
||||
sudo rm -rf /usr/lib/firefox /usr/bin/firefox*
|
||||
sudo rm -rf /usr/bin/google-chrome*
|
||||
# Cloud tools (Chrome and PowerShell live under these top-level dirs)
|
||||
sudo rm -rf /opt/microsoft /opt/google
|
||||
sudo rm -rf /opt/az /usr/share/az_*
|
||||
sudo rm -rf /usr/lib/google-cloud-sdk /opt/google-cloud-sdk /usr/bin/gcloud*
|
||||
# Databases (MySQL; MongoDB is not installed on ubuntu-24.04)
|
||||
sudo rm -rf /var/lib/mysql /etc/mysql /usr/sbin/mysqld
|
||||
# Other apt-managed packages now removed by rm -rf only
|
||||
sudo rm -rf /usr/lib/php /etc/php
|
||||
sudo rm -rf /usr/lib/mono /etc/mono
|
||||
sudo rm -rf /usr/lib/x86_64-linux-gnu/dri
|
||||
# Toolcaches and Kubernetes
|
||||
sudo rm -rf /opt/hostedtoolcache
|
||||
sudo rm -rf /usr/local/bin/minikube
|
||||
# Trailing apt cleanup: handles dangling deps and clears /var/cache/apt
|
||||
sudo apt-get autoremove -y || echo "::warning::apt-get autoremove failed"
|
||||
sudo apt-get clean || echo "::warning::apt-get clean failed"
|
||||
|
||||
- name: '[Linux] Show disk usage after cleaning'
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: |
|
||||
df -h
|
||||
echo "Top-level directories by size:"
|
||||
sudo du -h -d1 / 2>/dev/null | sort -hr | head -n 20 || true
|
||||
|
||||
- name: '[Linux] Assert free space and emit summary'
|
||||
if: runner.os == 'Linux' && always()
|
||||
shell: bash
|
||||
run: |
|
||||
free_kb=$(df --output=avail / | tail -1 | tr -d ' ')
|
||||
free_gb=$(awk "BEGIN{printf \"%.2f\", $free_kb/1024/1024}")
|
||||
before_kb=${__CLEAN_RUNNER_DISK_FREE_BEFORE_KB:-0}
|
||||
freed_gb=$(awk "BEGIN{printf \"%.2f\", ($free_kb-$before_kb)/1024/1024}")
|
||||
started_at=${__CLEAN_RUNNER_DISK_STARTED_AT:-$(date +%s)}
|
||||
elapsed=$(( $(date +%s) - started_at ))
|
||||
em=$((elapsed/60)); es=$((elapsed%60))
|
||||
echo "clean-runner-disk: freed ${freed_gb} GB in ${em}m ${es}s on Linux" >> $GITHUB_STEP_SUMMARY
|
||||
threshold_kb=$((20*1024*1024))
|
||||
if [ "$free_kb" -lt "$threshold_kb" ]; then
|
||||
echo "Top 5 directories by size:"
|
||||
sudo du -h -d1 / 2>/dev/null | sort -hr | head -n 5 || true
|
||||
echo "::error::clean-runner-disk: only ${free_gb} GB free on /, expected >= 20 GB."
|
||||
exit 1
|
||||
fi
|
||||
echo "clean-runner-disk: ${free_gb} GB free on / (threshold 20 GB) — OK"
|
||||
|
||||
# =========================================================================
|
||||
# Windows path
|
||||
# =========================================================================
|
||||
- name: '[Windows] Show disk usage before cleaning'
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
Get-PSDrive -PSProvider FileSystem | Format-Table -AutoSize
|
||||
$freeBytes = (Get-PSDrive C).Free
|
||||
"__CLEAN_RUNNER_DISK_FREE_BEFORE_BYTES=$freeBytes" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
"__CLEAN_RUNNER_DISK_STARTED_AT=$([DateTimeOffset]::UtcNow.ToUnixTimeSeconds())" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
# Flat listing only — recursive size scan before cleanup takes ~5 min on a loaded runner
|
||||
Get-ChildItem -Path C:\ -Directory -Force -ErrorAction SilentlyContinue |
|
||||
Select-Object Name, LastWriteTime | Format-Table -AutoSize
|
||||
|
||||
- name: '[Windows] Clean runner disk (parallel Remove-Item)'
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$paths = @(
|
||||
'C:\Android',
|
||||
'C:\hostedtoolcache',
|
||||
'C:\Program Files\dotnet',
|
||||
'C:\Program Files (x86)\dotnet',
|
||||
'C:\ghcup',
|
||||
'C:\Program Files\Haskell',
|
||||
'C:\Program Files\Haskell Platform',
|
||||
'C:\Strawberry',
|
||||
'C:\msys64',
|
||||
'C:\Miniconda',
|
||||
'C:\Program Files\MongoDB',
|
||||
'C:\Program Files\PostgreSQL',
|
||||
'C:\PostgreSQL',
|
||||
'C:\Program Files\Google\Chrome',
|
||||
'C:\Program Files (x86)\Google\Chrome',
|
||||
'C:\Program Files\Mozilla Firefox',
|
||||
'C:\selenium',
|
||||
'C:\SeleniumWebDrivers',
|
||||
'C:\vcpkg',
|
||||
'C:\tools'
|
||||
)
|
||||
|
||||
# ForEach-Object -Parallel (PS7) removes directories concurrently without
|
||||
# subprocess overhead. ThrottleLimit 8 keeps I/O pressure reasonable.
|
||||
$paths | Where-Object { Test-Path -LiteralPath $_ } | ForEach-Object -Parallel {
|
||||
$p = $_
|
||||
Write-Host "Removing $p"
|
||||
Remove-Item -LiteralPath $p -Recurse -Force -ErrorAction Continue
|
||||
if (Test-Path -LiteralPath $p) {
|
||||
Write-Host "::warning::$p still present after removal"
|
||||
}
|
||||
} -ThrottleLimit 8
|
||||
|
||||
- name: '[Windows] Show disk usage after cleaning'
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
Get-PSDrive -PSProvider FileSystem | Format-Table -AutoSize
|
||||
|
||||
- name: '[Windows] Assert free space and emit summary'
|
||||
if: runner.os == 'Windows' && always()
|
||||
shell: pwsh
|
||||
run: |
|
||||
$freeBytes = (Get-PSDrive C).Free
|
||||
$freeGb = [math]::Round($freeBytes / 1GB, 2)
|
||||
$beforeBytes = [int64]($env:__CLEAN_RUNNER_DISK_FREE_BEFORE_BYTES ?? '0')
|
||||
$freedGb = [math]::Round(($freeBytes - $beforeBytes) / 1GB, 2)
|
||||
$startedAt = [int64]($env:__CLEAN_RUNNER_DISK_STARTED_AT ?? [DateTimeOffset]::UtcNow.ToUnixTimeSeconds())
|
||||
$elapsed = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() - $startedAt
|
||||
$em = [math]::Floor($elapsed / 60); $es = $elapsed % 60
|
||||
"clean-runner-disk: freed $freedGb GB in ${em}m ${es}s on Windows" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append
|
||||
$thresholdGb = 40
|
||||
if ($freeGb -lt $thresholdGb) {
|
||||
Write-Host "Top 5 remaining directories on C: by size:"
|
||||
Get-ChildItem -Path C:\ -Directory -Force -ErrorAction SilentlyContinue |
|
||||
ForEach-Object {
|
||||
$size = (Get-ChildItem -Path $_.FullName -Recurse -Force -ErrorAction SilentlyContinue |
|
||||
Measure-Object -Property Length -Sum).Sum
|
||||
[PSCustomObject]@{ Path = $_.FullName; SizeGB = [math]::Round($size / 1GB, 2) }
|
||||
} | Sort-Object SizeGB -Descending | Select-Object -First 5 | Format-Table -AutoSize
|
||||
Write-Host "::error::clean-runner-disk: only $freeGb GB free on C:, expected >= $thresholdGb GB."
|
||||
exit 1
|
||||
}
|
||||
Write-Host "clean-runner-disk: $freeGb GB free on C: (threshold $thresholdGb GB) — OK"
|
||||
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
[resolutions."actions/checkout"."^6"]
|
||||
version = "v6.0.1"
|
||||
|
||||
[resolutions."actions/create-github-app-token"."^2"]
|
||||
version = "v2.2.1"
|
||||
|
||||
[resolutions."actions/download-artifact"."^4"]
|
||||
version = "v4"
|
||||
|
||||
[resolutions."actions/download-artifact"."^6"]
|
||||
version = "v6.0.0"
|
||||
|
||||
[resolutions."actions/download-artifact"."~6.0.0"]
|
||||
version = "v6.0.0"
|
||||
|
||||
[resolutions."actions/github-script"."^8"]
|
||||
version = "v8.0.0"
|
||||
|
||||
[resolutions."actions/upload-artifact"."^5"]
|
||||
version = "v5.0.0"
|
||||
|
||||
[resolutions."docker/build-push-action"."^6"]
|
||||
version = "v6.18.0"
|
||||
|
||||
[resolutions."docker/login-action"."^3"]
|
||||
version = "v3.6.0"
|
||||
|
||||
[resolutions."docker/metadata-action"."^5"]
|
||||
version = "v5.10.0"
|
||||
|
||||
[resolutions."docker/metadata-action"."~5.10.0"]
|
||||
version = "v5.10.0"
|
||||
|
||||
[resolutions."docker/metadata-action"."~5.7.0"]
|
||||
version = "v5.7.0"
|
||||
|
||||
[resolutions."docker/scout-action"."^1"]
|
||||
version = "v1.18.2"
|
||||
|
||||
[resolutions."docker/scout-action"."~1.18.2"]
|
||||
version = "v1.18.2"
|
||||
|
||||
[resolutions."docker/scout-action"."~1.20.4"]
|
||||
version = "v1.20.4"
|
||||
|
||||
[resolutions."docker/setup-buildx-action"."^3"]
|
||||
version = "v3.11.1"
|
||||
|
||||
[resolutions."github/codeql-action/upload-sarif"."^4"]
|
||||
version = "v4.31.7"
|
||||
|
||||
[resolutions."grafana/github-api-commit-action"."^1"]
|
||||
version = "v1.0.0"
|
||||
|
||||
[resolutions."jdx/mise-action"."^4"]
|
||||
version = "v4.0.1"
|
||||
|
||||
[resolutions."ossf/scorecard-action"."^2"]
|
||||
version = "v2.4.3"
|
||||
|
||||
[resolutions."peter-evans/create-pull-request"."^7"]
|
||||
version = "v7.0.11"
|
||||
|
||||
[resolutions."peter-evans/dockerhub-description"."^5"]
|
||||
version = "v5.0.0"
|
||||
|
||||
[resolutions."plexsystems/container-structure-test-action"."~0.3.0"]
|
||||
version = "v0.3.0"
|
||||
|
||||
[actions."actions/checkout"."v6.0.1"]
|
||||
sha = "8e8c483db84b4bee98b60c0593521ed34d9990e8"
|
||||
repository = "actions/checkout"
|
||||
ref_type = "tag"
|
||||
date = "2025-12-02T02:08:49Z"
|
||||
|
||||
[actions."actions/create-github-app-token"."v2.2.1"]
|
||||
sha = "29824e69f54612133e76f7eaac726eef6c875baf"
|
||||
repository = "actions/create-github-app-token"
|
||||
ref_type = "tag"
|
||||
date = "2025-12-05T22:53:03Z"
|
||||
|
||||
[actions."actions/download-artifact".v4]
|
||||
sha = "d3f86a106a0bac45b974a628896c90dbdf5c8093"
|
||||
repository = "actions/download-artifact"
|
||||
ref_type = "tag"
|
||||
date = "2025-04-24T16:25:03Z"
|
||||
|
||||
[actions."actions/download-artifact"."v6.0.0"]
|
||||
sha = "018cc2cf5baa6db3ef3c5f8a56943fffe632ef53"
|
||||
repository = "actions/download-artifact"
|
||||
ref_type = "tag"
|
||||
date = "2025-10-24T18:15:38Z"
|
||||
|
||||
[actions."actions/github-script"."v8.0.0"]
|
||||
sha = "ed597411d8f924073f98dfc5c65a23a2325f34cd"
|
||||
repository = "actions/github-script"
|
||||
ref_type = "tag"
|
||||
date = "2025-09-04T14:48:16Z"
|
||||
|
||||
[actions."actions/upload-artifact"."v5.0.0"]
|
||||
sha = "330a01c490aca151604b8cf639adc76d48f6c5d4"
|
||||
repository = "actions/upload-artifact"
|
||||
ref_type = "tag"
|
||||
date = "2025-10-24T18:15:34Z"
|
||||
|
||||
[actions."docker/build-push-action"."v6.18.0"]
|
||||
sha = "263435318d21b8e681c14492fe198d362a7d2c83"
|
||||
repository = "docker/build-push-action"
|
||||
ref_type = "tag"
|
||||
date = "2025-05-27T16:32:33Z"
|
||||
|
||||
[actions."docker/login-action"."v3.6.0"]
|
||||
sha = "5e57cd118135c172c3672efd75eb46360885c0ef"
|
||||
repository = "docker/login-action"
|
||||
ref_type = "tag"
|
||||
date = "2025-09-29T10:29:19Z"
|
||||
|
||||
[actions."docker/metadata-action"."v5.10.0"]
|
||||
sha = "c299e40c65443455700f0fdfc63efafe5b349051"
|
||||
repository = "docker/metadata-action"
|
||||
ref_type = "tag"
|
||||
date = "2025-11-27T12:36:24Z"
|
||||
|
||||
[actions."docker/metadata-action"."v5.7.0"]
|
||||
sha = "902fa8ec7d6ecbf8d84d538b9b233a880e428804"
|
||||
repository = "docker/metadata-action"
|
||||
ref_type = "release"
|
||||
date = "2025-02-26T15:31:35Z"
|
||||
|
||||
[actions."docker/scout-action"."v1.18.2"]
|
||||
sha = "f8c776824083494ab0d56b8105ba2ca85c86e4de"
|
||||
repository = "docker/scout-action"
|
||||
ref_type = "tag"
|
||||
date = "2025-07-21T12:52:07Z"
|
||||
|
||||
[actions."docker/scout-action"."v1.20.4"]
|
||||
sha = "bacf462e8d090c09660de30a6ccc718035f961e3"
|
||||
repository = "docker/scout-action"
|
||||
ref_type = "release"
|
||||
date = "2026-04-08T08:04:55Z"
|
||||
|
||||
[actions."docker/setup-buildx-action"."v3.11.1"]
|
||||
sha = "e468171a9de216ec08956ac3ada2f0791b6bd435"
|
||||
repository = "docker/setup-buildx-action"
|
||||
ref_type = "tag"
|
||||
date = "2025-06-18T08:37:30Z"
|
||||
|
||||
[actions."github/codeql-action/upload-sarif"."v4.31.7"]
|
||||
sha = "cf1bb45a277cb3c205638b2cd5c984db1c46a412"
|
||||
repository = "github/codeql-action"
|
||||
ref_type = "tag"
|
||||
date = "2025-12-05T17:17:21Z"
|
||||
|
||||
[actions."grafana/github-api-commit-action"."v1.0.0"]
|
||||
sha = "b1d81091e8480dd11fcea8bc1f0ab977a0376ca5"
|
||||
repository = "grafana/github-api-commit-action"
|
||||
ref_type = "tag"
|
||||
date = "2025-05-07T18:28:02Z"
|
||||
|
||||
[actions."jdx/mise-action"."v4.0.1"]
|
||||
sha = "1648a7812b9aeae629881980618f079932869151"
|
||||
repository = "jdx/mise-action"
|
||||
ref_type = "tag"
|
||||
date = "2026-03-22T16:06:38Z"
|
||||
|
||||
[actions."ossf/scorecard-action"."v2.4.3"]
|
||||
sha = "4eaacf0543bb3f2c246792bd56e8cdeffafb205a"
|
||||
repository = "ossf/scorecard-action"
|
||||
ref_type = "tag"
|
||||
date = "2025-09-30T20:36:00Z"
|
||||
|
||||
[actions."peter-evans/create-pull-request"."v7.0.11"]
|
||||
sha = "22a9089034f40e5a961c8808d113e2c98fb63676"
|
||||
repository = "peter-evans/create-pull-request"
|
||||
ref_type = "tag"
|
||||
date = "2025-12-05T17:14:47Z"
|
||||
|
||||
[actions."peter-evans/dockerhub-description"."v5.0.0"]
|
||||
sha = "1b9a80c056b620d92cedb9d9b5a223409c68ddfa"
|
||||
repository = "peter-evans/dockerhub-description"
|
||||
ref_type = "tag"
|
||||
date = "2025-10-01T13:39:49Z"
|
||||
|
||||
[actions."plexsystems/container-structure-test-action"."v0.3.0"]
|
||||
sha = "c0a028aa96e8e82ae35be556040340cbb3e280ca"
|
||||
repository = "plexsystems/container-structure-test-action"
|
||||
ref_type = "tag"
|
||||
date = "2023-03-30T18:19:59Z"
|
||||
@@ -0,0 +1,23 @@
|
||||
[actions]
|
||||
"actions/checkout" = "^6"
|
||||
"actions/create-github-app-token" = "^2"
|
||||
"actions/download-artifact" = "^6"
|
||||
"actions/github-script" = "^8"
|
||||
"actions/upload-artifact" = "^5"
|
||||
"docker/build-push-action" = "^6"
|
||||
"docker/login-action" = "^3"
|
||||
"docker/metadata-action" = "^5"
|
||||
"docker/scout-action" = "^1"
|
||||
"docker/setup-buildx-action" = "^3"
|
||||
"github/codeql-action/upload-sarif" = "^4"
|
||||
"grafana/github-api-commit-action" = "^1"
|
||||
"jdx/mise-action" = "^4"
|
||||
"ossf/scorecard-action" = "^2"
|
||||
"peter-evans/create-pull-request" = "^7"
|
||||
"peter-evans/dockerhub-description" = "^5"
|
||||
"plexsystems/container-structure-test-action" = "~0.3.0"
|
||||
|
||||
[actions.overrides]
|
||||
"actions/download-artifact" = [{ workflow = ".github/workflows/build.yml", job = "test_image", step = 2, version = "^4" }, { workflow = ".github/workflows/build.yml", job = "scan_image", step = 0, version = "^4" }, { workflow = ".github/workflows/update_version.yml", job = "validate_config_version", step = 2, version = "~6.0.0" }, { workflow = ".github/workflows/update_version.yml", job = "update_docs_and_create_pr", step = 2, version = "~6.0.0" }, { workflow = ".github/workflows/update_version.yml", job = "update_android_version", step = 2, version = "~6.0.0" }, { workflow = ".github/workflows/update_version.yml", job = "update_windows_version", step = 3, version = "~6.0.0" }, { workflow = ".github/workflows/update_version.yml", job = "update_docs_and_create_pr", step = 6, version = "~6.0.0" }]
|
||||
"docker/metadata-action" = [ { workflow = ".github/workflows/ci.yml", job = "test_image", step = 5, version = "~5.10.0" }, { workflow = ".github/workflows/release.yml", job = "release_android", step = 2, version = "~5.10.0" }, { workflow = ".github/workflows/windows.yml", job = "test_windows", step = 3, version = "~5.7.0" }, { workflow = ".github/workflows/windows.yml", job = "test_windows", step = 4, version = "~5.7.0" }, { workflow = ".github/workflows/release.yml", job = "release_windows", step = 2, version = "~5.10.0" }, { workflow = ".github/workflows/build.yml", job = "build_image", step = 7, version = "~5.10.0" }]
|
||||
"docker/scout-action" = [{ workflow = ".github/workflows/build.yml", job = "scan_image", step = 3, version = "~1.20.4" }, { workflow = ".github/workflows/release.yml", job = "record_image", step = 2, version = "~1.18.2" }, { workflow = ".github/workflows/build.yml", job = "scan_image", step = 5, version = "~1.20.4" }]
|
||||
+19
-9
@@ -9,22 +9,32 @@
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "Schedule Github Actions updates on the first day of the month",
|
||||
"description": "Disable Renovate's built-in github-actions manager — gx owns workflow and composite-action files",
|
||||
"matchManagers": ["github-actions"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"description": "Schedule gx.toml-driven action major upgrades on the first day of the month",
|
||||
"matchFileNames": [".github/gx.toml"],
|
||||
"groupName": "github-actions",
|
||||
"matchDatasources": [
|
||||
"github-tags"
|
||||
],
|
||||
"schedule": [
|
||||
"* 0-3 1 * *"
|
||||
]
|
||||
"schedule": ["* 0-3 1 * *"]
|
||||
}
|
||||
],
|
||||
"customManagers": [
|
||||
{
|
||||
"customType": "regex",
|
||||
"fileMatch": [
|
||||
"^Dockerfile$"
|
||||
"description": "Read GitHub Action specifiers from gx.toml",
|
||||
"managerFilePatterns": ["/^\\.github/gx\\.toml$/"],
|
||||
"matchStrings": [
|
||||
"\"(?<depName>(?<packageName>[^/\"]+/[^/\"]+)(?:/[^\"]+)?)\"\\s*=\\s*\"(?<currentValue>[^\"]+)\""
|
||||
],
|
||||
"datasourceTemplate": "github-tags",
|
||||
"versioningTemplate": "npm",
|
||||
"extractVersionTemplate": "^v?(?<version>.+)$"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": ["/^Dockerfile$/"],
|
||||
"matchStrings": [
|
||||
"#\\s*renovate:\\s*suite=(?<suite>.*?) depName=(?<depName>.*?)\\sARG .*?_VERSION=\"(?<currentValue>.*)\"\\s"
|
||||
],
|
||||
|
||||
+215
-69
@@ -11,15 +11,40 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test_image:
|
||||
permissions:
|
||||
# Allow to write packages for the docker/scout-action to write a comment
|
||||
packages: write
|
||||
# Allow to write pull requests for the docker/scout-action to write a comment
|
||||
pull-requests: write
|
||||
# Allow to write security events for github/codeql-action/upload-sarif to upload SARIF results
|
||||
security-events: write
|
||||
# Resolves the bootstrap container tag — the latest published release tag.
|
||||
# Reading from the releases API (instead of vars.FLUTTER_VERSION) makes
|
||||
# fork PRs work, and reading from releases (instead of main's version.json)
|
||||
# avoids the window between merging a version-bump PR and publishing its
|
||||
# image during which main's version.json points to a tag that doesn't
|
||||
# exist in GHCR yet.
|
||||
setup:
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
flutter_version: ${{ steps.read.outputs.version }}
|
||||
steps:
|
||||
- name: Read latest release tag
|
||||
id: read
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
version=$(gh api "repos/${{ github.repository }}/releases/latest" --jq '.tag_name')
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build_image:
|
||||
permissions:
|
||||
packages: write
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
# image_ref is set per the p2 contract but GitHub Actions suppresses
|
||||
# any job output whose value contains a registered secret. When
|
||||
# github.repository_owner equals DOCKER_HUB_USERNAME (as on this repo),
|
||||
# image_ref is masked-and-dropped. Consumers MUST use image_tag and
|
||||
# reconstruct ghcr.io/${{ github.repository_owner }}/flutter-android:<tag>.
|
||||
image_ref: ${{ steps.handoff.outputs.is_fork != 'true' && format('ghcr.io/{0}/flutter-android:{1}', github.repository_owner, steps.handoff.outputs.tag) || '' }}
|
||||
image_tag: ${{ steps.handoff.outputs.is_fork != 'true' && steps.handoff.outputs.tag || '' }}
|
||||
image_artifact: ${{ steps.handoff.outputs.is_fork == 'true' && format('image-{0}', github.run_id) || '' }}
|
||||
image_local_tag: ${{ steps.local_tag.outputs.ref }}
|
||||
flutter_version: ${{ env.FLUTTER_VERSION }}
|
||||
env:
|
||||
IMAGE_REPOSITORY_NAME: flutter-android
|
||||
VERSION_MANIFEST: config/version.json
|
||||
@@ -44,12 +69,38 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
# Secrets are not available to PRs from forks, so skip the Docker Hub
|
||||
# login for those runs. The build still works against public base images.
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Compute handoff tag
|
||||
id: handoff
|
||||
env:
|
||||
IS_PR: ${{ github.event_name == 'pull_request' }}
|
||||
IS_FORK: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
echo "is_fork=$IS_FORK" >> "$GITHUB_OUTPUT"
|
||||
if [[ "$IS_PR" == "true" ]]; then
|
||||
echo "tag=pr-$PR_NUMBER" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "tag=branch-${REF_NAME//\//-}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Load image metadata
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
id: metadata
|
||||
@@ -59,13 +110,17 @@ jobs:
|
||||
tags: |
|
||||
type=raw,value=${{ env.FLUTTER_VERSION }}
|
||||
|
||||
# outputs is just `type=docker` (load into local daemon). The GHCR
|
||||
# handoff push is done by an explicit `docker push` step below — when
|
||||
# combined with `type=docker`, buildkit silently ignores a `type=registry`
|
||||
# output and pushes only via --tag (which points at docker.io, not GHCR).
|
||||
- name: Build image and push to local Docker daemon
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
file: android.Dockerfile
|
||||
load: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
outputs: type=docker
|
||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/flutter-android:buildcache
|
||||
cache-to: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && format('type=registry,ref=ghcr.io/{0}/flutter-android:buildcache,mode=max', github.repository_owner) || '' }}
|
||||
labels: ${{ steps.metadata.outputs.labels }}
|
||||
tags: ${{ steps.metadata.outputs.tags }}
|
||||
target: android
|
||||
@@ -77,29 +132,138 @@ jobs:
|
||||
android_ndk_version=${{ env.ANDROID_NDK_VERSION }}
|
||||
cmake_version=${{ env.CMAKE_VERSION }}
|
||||
|
||||
# Re-tag the loaded image to a name that does NOT contain the owner.
|
||||
# `image_local_tag` is exposed as a job output; GitHub Actions drops
|
||||
# outputs whose value contains a registered secret (DOCKER_HUB_USERNAME
|
||||
# == github.repository_owner on this repo), so the metadata tag
|
||||
# `<owner>/flutter-android:<version>` cannot be passed through.
|
||||
- name: Re-tag image for local handoff
|
||||
id: local_tag
|
||||
run: |
|
||||
DEST="flutter-android:${{ env.FLUTTER_VERSION }}"
|
||||
docker tag "${{ fromJSON(steps.metadata.outputs.json).tags[0] }}" "$DEST"
|
||||
echo "ref=$DEST" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Push image to GHCR
|
||||
if: steps.handoff.outputs.is_fork != 'true'
|
||||
run: |
|
||||
GHCR_REF="ghcr.io/${{ github.repository_owner }}/flutter-android:${{ steps.handoff.outputs.tag }}"
|
||||
docker tag "${{ fromJSON(steps.metadata.outputs.json).tags[0] }}" "$GHCR_REF"
|
||||
docker push "$GHCR_REF"
|
||||
|
||||
- name: Save image as artifact
|
||||
if: steps.handoff.outputs.is_fork == 'true'
|
||||
run: docker save "${{ steps.local_tag.outputs.ref }}" | gzip > image.tar.gz
|
||||
|
||||
- name: Upload image artifact
|
||||
if: steps.handoff.outputs.is_fork == 'true'
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: image-${{ github.run_id }}
|
||||
path: image.tar.gz
|
||||
retention-days: 1
|
||||
compression-level: 0
|
||||
|
||||
test_image:
|
||||
needs: build_image
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Clean runner disk
|
||||
if: needs.build_image.outputs.image_artifact != ''
|
||||
uses: ./.github/actions/clean-runner-disk
|
||||
|
||||
- name: Download image artifact
|
||||
if: needs.build_image.outputs.image_artifact != ''
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: ${{ needs.build_image.outputs.image_artifact }}
|
||||
|
||||
- name: Load image from artifact
|
||||
if: needs.build_image.outputs.image_artifact != ''
|
||||
run: gunzip -c image.tar.gz | docker load
|
||||
|
||||
- name: Login to GHCR
|
||||
if: needs.build_image.outputs.image_artifact == ''
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Pull image from registry
|
||||
if: needs.build_image.outputs.image_artifact == ''
|
||||
run: docker pull "ghcr.io/${{ github.repository_owner }}/flutter-android:${{ needs.build_image.outputs.image_tag }}"
|
||||
|
||||
- name: Test image
|
||||
uses: plexsystems/container-structure-test-action@c0a028aa96e8e82ae35be556040340cbb3e280ca # v0.3.0
|
||||
with:
|
||||
image: ${{ fromJSON(steps.metadata.outputs.json).tags[0] }}
|
||||
image: ${{ needs.build_image.outputs.image_artifact != '' && needs.build_image.outputs.image_local_tag || format('ghcr.io/{0}/flutter-android:{1}', github.repository_owner, needs.build_image.outputs.image_tag) }}
|
||||
config: test/android.yml
|
||||
|
||||
# TODO: Parallelize testing and vulnerability scanning
|
||||
scan_image:
|
||||
needs: build_image
|
||||
runs-on: ubuntu-24.04
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
permissions:
|
||||
packages: read
|
||||
pull-requests: write
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Download image artifact
|
||||
if: needs.build_image.outputs.image_artifact != ''
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: ${{ needs.build_image.outputs.image_artifact }}
|
||||
|
||||
- name: Load image from artifact
|
||||
if: needs.build_image.outputs.image_artifact != ''
|
||||
run: gunzip -c image.tar.gz | docker load
|
||||
|
||||
# Docker Hub login is required by Scout — it authenticates against the
|
||||
# Docker Hub identity tied to the org secret, not the GHCR identity.
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
if: needs.build_image.outputs.image_artifact == ''
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Pull the PR-tagged GHCR image and re-tag it with the Docker Hub repo
|
||||
# identity (<owner>/flutter-android:<version>). Scout's `compare` looks
|
||||
# up the image's repo in its environment records — those records exist
|
||||
# for the Docker Hub repo, not for `ghcr.io/<owner>/flutter-android`.
|
||||
# Without this re-tag, Scout fails with "not in stream environment:prod".
|
||||
- name: Pull image and re-tag for Scout
|
||||
if: needs.build_image.outputs.image_artifact == ''
|
||||
run: |
|
||||
GHCR_REF="ghcr.io/${{ github.repository_owner }}/flutter-android:${{ needs.build_image.outputs.image_tag }}"
|
||||
SCOUT_REF="${{ github.repository_owner }}/flutter-android:${{ needs.build_image.outputs.flutter_version }}"
|
||||
docker pull "$GHCR_REF"
|
||||
docker tag "$GHCR_REF" "$SCOUT_REF"
|
||||
|
||||
- name: Scan with Docker Scout
|
||||
id: docker-scout
|
||||
uses: docker/scout-action@f8c776824083494ab0d56b8105ba2ca85c86e4de # v1.18.2
|
||||
uses: docker/scout-action@bacf462e8d090c09660de30a6ccc718035f961e3 # v1.20.4
|
||||
with:
|
||||
command: compare, recommendations
|
||||
# Use the Docker Hub image that is the first tag in the metadata
|
||||
image: local://${{ fromJson(steps.metadata.outputs.json).tags[0] }}
|
||||
# github-token is needed to be able to write the PR comment
|
||||
image: local://${{ github.repository_owner }}/flutter-android:${{ needs.build_image.outputs.flutter_version }}
|
||||
github-token: ${{ github.token }}
|
||||
only-fixed: true
|
||||
organization: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
# sarif-file: output.sarif.json
|
||||
to-env: prod
|
||||
# Enable debug logging when needed
|
||||
# debug: true
|
||||
# verbose-debug: true
|
||||
|
||||
validate_version_files:
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -107,17 +271,13 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup CUE
|
||||
uses: jaxxstorm/action-install-gh-release@6096f2a2bbfee498ced520b6922ac2c06e990ed2 # v2.1.0
|
||||
with:
|
||||
repo: cue-lang/cue
|
||||
tag: v0.15.0
|
||||
digest: 06925fc1e5174591cef0b1e42ac32cff4271804742cd20893de1793b6d82d460
|
||||
- name: Setup mise tools
|
||||
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
|
||||
|
||||
- name: Validate version.json and flutter_version.json with CUE
|
||||
run: |
|
||||
cue vet config/version.cue -d '#FlutterVersion' config/flutter_version.json
|
||||
cue vet config/version.cue -d '#Version' config/version.json
|
||||
cue vet config/schema.cue -d '#FlutterVersion' config/flutter_version.json
|
||||
cue vet config/schema.cue -d '#Version' config/version.json
|
||||
|
||||
validate_generated_config:
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -125,12 +285,8 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup CUE
|
||||
uses: jaxxstorm/action-install-gh-release@6096f2a2bbfee498ced520b6922ac2c06e990ed2 # v2.1.0
|
||||
with:
|
||||
repo: cue-lang/cue
|
||||
tag: v0.15.0
|
||||
digest: 06925fc1e5174591cef0b1e42ac32cff4271804742cd20893de1793b6d82d460
|
||||
- name: Setup mise tools
|
||||
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
|
||||
|
||||
- name: Generate test files with CUE
|
||||
run: |
|
||||
@@ -146,46 +302,38 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
- name: Setup NodeJS
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
cache: npm
|
||||
cache-dependency-path: docs/src/package-lock.json
|
||||
node-version: lts/*
|
||||
- name: Setup mise tools
|
||||
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
|
||||
|
||||
- name: Generate authentication token with GitHub App to trigger Actions
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.VERIFIED_COMMIT_ID }}
|
||||
private-key: ${{ secrets.VERIFIED_COMMIT_KEY }}
|
||||
repositories: ${{ github.event.repository.name }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
|
||||
- name: Update documentation
|
||||
- name: Build documentation
|
||||
working-directory: docs/src
|
||||
run: |
|
||||
npm ci --prefer-offline
|
||||
npm run build
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm run build
|
||||
|
||||
- name: Commit and push documentation
|
||||
uses: grafana/github-api-commit-action@b1d81091e8480dd11fcea8bc1f0ab977a0376ca5 # v1.0.0
|
||||
# Upload generated docs so reviewers can inspect them. Commit-back is
|
||||
# handled by update_docs.yml on push to main — pushing to fork PR
|
||||
# branches is not possible with base-repo credentials.
|
||||
- name: Upload built docs
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
commit-message: 'docs: generate documentation files'
|
||||
success-if-no-changes: true
|
||||
stage-all-files: true
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
name: docs-${{ github.event.pull_request.number || github.sha }}
|
||||
path: |
|
||||
readme.md
|
||||
LICENSE.md
|
||||
docs/contributing.md
|
||||
docs/windows.md
|
||||
retention-days: 14
|
||||
|
||||
test_gradle:
|
||||
needs: setup
|
||||
permissions:
|
||||
# Allow to read packages to pull the container image from GitHub Container Registry
|
||||
packages: read
|
||||
runs-on: ubuntu-24.04
|
||||
container:
|
||||
image: ghcr.io/${{ github.repository_owner }}/flutter-android:${{ vars.FLUTTER_VERSION }}
|
||||
image: ghcr.io/${{ github.repository_owner }}/flutter-android:${{ needs.setup.outputs.flutter_version }}
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
@@ -219,16 +367,14 @@ jobs:
|
||||
|
||||
- name: Update default Android platform versions in Flutter
|
||||
working-directory: test_app/android
|
||||
env:
|
||||
BUILD_TOOLS_VERSION: ${{ fromJson(steps.version-json.outputs.content).android.buildTools.version }}
|
||||
run: |
|
||||
cat ../../script/updateAndroidVersions.gradle.kts >> app/build.gradle.kts
|
||||
./gradlew --warning-mode all updateAndroidVersions
|
||||
|
||||
- name: Setup CUE
|
||||
uses: jaxxstorm/action-install-gh-release@6096f2a2bbfee498ced520b6922ac2c06e990ed2 # v2.1.0
|
||||
with:
|
||||
repo: cue-lang/cue
|
||||
tag: v0.15.0
|
||||
digest: 06925fc1e5174591cef0b1e42ac32cff4271804742cd20893de1793b6d82d460
|
||||
- name: Setup mise tools
|
||||
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
|
||||
|
||||
- name: Validate version.json with CUE
|
||||
run: cue vet config/version.cue -d '#Version' config/version.json
|
||||
run: cue vet config/schema.cue -d '#Version' config/version.json
|
||||
|
||||
@@ -22,12 +22,8 @@ jobs:
|
||||
# Fetch all tags to use as input for the changelog generation
|
||||
fetch-tags: true
|
||||
|
||||
- name: Setup git-cliff
|
||||
uses: jaxxstorm/action-install-gh-release@6096f2a2bbfee498ced520b6922ac2c06e990ed2 # v2.1.0
|
||||
with:
|
||||
repo: orhun/git-cliff
|
||||
tag: v2.10.1
|
||||
digest: 6abe8a3e112b266dd00abf9089a8052ddf7314f39e1c53e192edf0652abca4eb
|
||||
- name: Setup mise tools
|
||||
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
|
||||
|
||||
- name: Read environment variables from the version manifest
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
|
||||
@@ -29,12 +29,8 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
|
||||
- name: Setup CUE
|
||||
uses: jaxxstorm/action-install-gh-release@6096f2a2bbfee498ced520b6922ac2c06e990ed2 # v2.1.0
|
||||
with:
|
||||
repo: cue-lang/cue
|
||||
tag: v0.15.0
|
||||
digest: 06925fc1e5174591cef0b1e42ac32cff4271804742cd20893de1793b6d82d460
|
||||
- name: Setup mise tools
|
||||
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
|
||||
|
||||
- name: Read environment variables from the version manifest
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
name: Cleanup PR image tag
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
delete:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: cleanup-pr-image-${{ github.event.pull_request.number || github.event.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
# On `delete` events, only act on branch deletions (not tag deletions).
|
||||
# `pull_request: closed` is always in scope.
|
||||
if: ${{ github.event_name == 'pull_request' || github.event.ref_type == 'branch' }}
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
env:
|
||||
PACKAGE_NAME: flutter-android
|
||||
steps:
|
||||
- name: Compute target tag
|
||||
id: tag
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
REF_NAME: ${{ github.event.ref }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ "$EVENT_NAME" == "pull_request" ]]; then
|
||||
target="pr-${PR_NUMBER}"
|
||||
else
|
||||
target="branch-${REF_NAME//\//-}"
|
||||
fi
|
||||
# Defense in depth: fail closed if the computed tag does not match
|
||||
# the documented handoff-tag regex. Release tags (`<version>`) and
|
||||
# `buildcache` MUST be unreachable from this code path.
|
||||
if [[ ! "$target" =~ ^pr-[0-9]+$ ]] && [[ ! "$target" =~ ^branch-[A-Za-z0-9._-]+$ ]]; then
|
||||
echo "::error::computed tag '$target' does not match handoff-tag regex; refusing to proceed"
|
||||
exit 1
|
||||
fi
|
||||
echo "target=$target" >> "$GITHUB_OUTPUT"
|
||||
echo "Computed target tag: $target"
|
||||
|
||||
- name: Resolve and delete GHCR version
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TARGET_TAG: ${{ steps.tag.outputs.target }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# The package is user-owned (gmeligio); /user/packages/... is the
|
||||
# authenticated-user endpoint. The workflow GITHUB_TOKEN can delete
|
||||
# because the package -> repo Actions Access role is Admin.
|
||||
version_id=$(gh api \
|
||||
"/user/packages/container/${PACKAGE_NAME}/versions" \
|
||||
--paginate \
|
||||
--jq ".[] | select(.metadata.container.tags[]? == \"${TARGET_TAG}\") | .id" \
|
||||
| head -n 1)
|
||||
|
||||
if [[ -z "$version_id" ]]; then
|
||||
echo "tag not found, nothing to delete (tag='${TARGET_TAG}')"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Deleting version_id=${version_id} tag=${TARGET_TAG}"
|
||||
status=$(curl -sS -o /tmp/delete-body -w '%{http_code}' \
|
||||
-X DELETE \
|
||||
-H "Authorization: Bearer ${GH_TOKEN}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"https://api.github.com/user/packages/container/${PACKAGE_NAME}/versions/${version_id}")
|
||||
|
||||
case "$status" in
|
||||
204)
|
||||
echo "Deleted ${TARGET_TAG} (version_id=${version_id})"
|
||||
;;
|
||||
404)
|
||||
echo "Version ${version_id} already gone (404); treating as idempotent success"
|
||||
;;
|
||||
*)
|
||||
echo "::error::DELETE returned HTTP ${status} for version_id=${version_id}"
|
||||
cat /tmp/delete-body
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,79 @@
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/**
|
||||
- .github/actions/**
|
||||
- .github/gx.toml
|
||||
- .github/gx.lock
|
||||
workflow_dispatch:
|
||||
|
||||
# Read-only by default; the tidy job's auto-fix step uses a GitHub App token.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup mise tools
|
||||
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
|
||||
|
||||
- name: Lint pinned GitHub Actions
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: gx lint
|
||||
|
||||
tidy:
|
||||
# Skip PRs from forks: the App token below is scoped to the upstream repo
|
||||
# and the fork branch is on the contributor's repo, so the push would fail.
|
||||
# Those PRs still get lint feedback and must run `gx tidy` locally.
|
||||
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Generate authentication token with GitHub App to trigger Actions
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.VERIFIED_COMMIT_ID }}
|
||||
private-key: ${{ secrets.VERIFIED_COMMIT_KEY }}
|
||||
repositories: ${{ github.event.repository.name }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
|
||||
- name: Checkout PR branch
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Setup mise tools
|
||||
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
|
||||
|
||||
- name: Run gx tidy
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
run: gx tidy
|
||||
|
||||
- name: Detect lock drift
|
||||
id: drift
|
||||
run: |
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Commit and push lock updates if any
|
||||
if: steps.drift.outputs.changed == 'true'
|
||||
uses: grafana/github-api-commit-action@b1d81091e8480dd11fcea8bc1f0ab977a0376ca5 # v1.0.0
|
||||
with:
|
||||
commit-message: "chore(deps): sync .github/gx.lock via gx tidy"
|
||||
stage-all-files: true
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
@@ -13,6 +13,7 @@ env:
|
||||
|
||||
jobs:
|
||||
release_android:
|
||||
if: github.event_name == 'push'
|
||||
permissions:
|
||||
# Allow to write packages to push the container image to the Github Container Registry
|
||||
packages: write
|
||||
@@ -49,6 +50,8 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
with:
|
||||
buildkitd-flags: --debug
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
@@ -88,6 +91,91 @@ jobs:
|
||||
android_ndk_version=${{ env.ANDROID_NDK_VERSION }}
|
||||
cmake_version=${{ env.CMAKE_VERSION }}
|
||||
|
||||
release_windows:
|
||||
permissions:
|
||||
# Allow to write packages to push the container image to the Github Container Registry
|
||||
packages: write
|
||||
runs-on: windows-2025
|
||||
env:
|
||||
IMAGE_REPOSITORY_NAME: flutter-windows
|
||||
VERSION_MANIFEST: config/version.json
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Read environment variables from the version manifest
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
env:
|
||||
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
|
||||
IMAGE_REPOSITORY_NAME: ${{ env.IMAGE_REPOSITORY_NAME }}
|
||||
VERSION_MANIFEST: ${{ env.VERSION_MANIFEST }}
|
||||
with:
|
||||
script: |
|
||||
const script = require('./script/setEnvironmentVariables.js')
|
||||
return await script({ core })
|
||||
|
||||
- name: Load image metadata
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
id: metadata
|
||||
with:
|
||||
images: |
|
||||
${{ env.IMAGE_REPOSITORY_PATH }}
|
||||
ghcr.io/${{ env.IMAGE_REPOSITORY_PATH }}
|
||||
quay.io/${{ env.IMAGE_REPOSITORY_PATH }}
|
||||
tags: |
|
||||
type=raw,value=${{ env.FLUTTER_VERSION }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
|
||||
- name: Login to Quay.io
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: quay.io
|
||||
username: ${{ secrets.QUAY_USERNAME }}
|
||||
password: ${{ secrets.QUAY_ROBOT_TOKEN }}
|
||||
|
||||
- name: Build and push image to registries
|
||||
shell: powershell
|
||||
env:
|
||||
METADATA_TAGS: ${{ steps.metadata.outputs.tags }}
|
||||
METADATA_LABELS: ${{ steps.metadata.outputs.labels }}
|
||||
run: |
|
||||
$tags = $env:METADATA_TAGS -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ }
|
||||
$labels = $env:METADATA_LABELS -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ }
|
||||
|
||||
$tagArgs = $tags | ForEach-Object { "--tag=$_" }
|
||||
$labelArgs = $labels | ForEach-Object { "--label=$_" }
|
||||
|
||||
$buildArgs = $tagArgs + $labelArgs + @(
|
||||
'--build-arg', "flutter_version=${{ env.FLUTTER_VERSION }}",
|
||||
'--build-arg', "git_version=${{ env.GIT_VERSION }}",
|
||||
'--build-arg', "vs_cmake_version=${{ env.VS_CMAKE_VERSION }}",
|
||||
'--build-arg', "vs_win11sdk_build=${{ env.VS_WIN11SDK_BUILD }}",
|
||||
'--build-arg', "vs_vctools_version=${{ env.VS_VCTOOLS_VERSION }}",
|
||||
'--target', 'flutter',
|
||||
'--file', 'windows.Dockerfile',
|
||||
'.'
|
||||
)
|
||||
|
||||
docker build @buildArgs
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
|
||||
foreach ($tag in $tags) {
|
||||
docker push $tag
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
}
|
||||
|
||||
update_description:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: release_android
|
||||
@@ -151,28 +239,6 @@ jobs:
|
||||
with:
|
||||
sarif_file: sarif.json
|
||||
|
||||
set_bootstrap_image:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: release_android
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Generate authentication token with GitHub App to trigger Actions
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.VERIFIED_COMMIT_ID }}
|
||||
private-key: ${{ secrets.VERIFIED_COMMIT_KEY }}
|
||||
repositories: ${{ github.event.repository.name }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
|
||||
- name: Update bootstrap image tag in environment variable
|
||||
run: gh variable set FLUTTER_VERSION --body "${{ env.FLUTTER_VERSION }}"
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
create_github_release:
|
||||
permissions:
|
||||
# Allow to create releases and upload assets to them
|
||||
@@ -189,12 +255,8 @@ jobs:
|
||||
# Fetch all tags to use as input for the changelog generation
|
||||
fetch-tags: true
|
||||
|
||||
- name: Setup git-cliff
|
||||
uses: jaxxstorm/action-install-gh-release@6096f2a2bbfee498ced520b6922ac2c06e990ed2 # v2.1.0
|
||||
with:
|
||||
repo: orhun/git-cliff
|
||||
tag: v2.10.1
|
||||
digest: 6abe8a3e112b266dd00abf9089a8052ddf7314f39e1c53e192edf0652abca4eb
|
||||
- name: Setup mise tools
|
||||
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
|
||||
|
||||
- name: Get the tag details
|
||||
id: get-tag-details
|
||||
|
||||
@@ -39,7 +39,6 @@ jobs:
|
||||
- name: Create Tag for a New Flutter Version
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
env:
|
||||
OLD_FLUTTER_VERSION: ${{ vars.FLUTTER_VERSION }}
|
||||
NEW_FLUTTER_VERSION: ${{ env.FLUTTER_VERSION }}
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- docs/src/**
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
update_docs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup mise tools
|
||||
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
|
||||
|
||||
- name: Build documentation
|
||||
working-directory: docs/src
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm run build
|
||||
|
||||
- name: Generate authentication token with GitHub App to trigger Actions
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.VERIFIED_COMMIT_ID }}
|
||||
private-key: ${{ secrets.VERIFIED_COMMIT_KEY }}
|
||||
repositories: ${{ github.event.repository.name }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
|
||||
- name: Commit and push regenerated documentation
|
||||
uses: grafana/github-api-commit-action@b1d81091e8480dd11fcea8bc1f0ab977a0376ca5 # v1.0.0
|
||||
with:
|
||||
commit-message: 'docs: regenerate documentation files'
|
||||
success-if-no-changes: true
|
||||
stage-all-files: true
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
@@ -7,7 +7,24 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION_PATH: config/flutter_version.json
|
||||
|
||||
jobs:
|
||||
# Resolves the bootstrap container tag — the latest published release tag.
|
||||
setup:
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
flutter_version: ${{ steps.read.outputs.version }}
|
||||
steps:
|
||||
- name: Read latest release tag
|
||||
id: read
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
version=$(gh api "repos/${{ github.repository }}/releases/latest" --jq '.tag_name')
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
|
||||
update_flutter_version:
|
||||
permissions:
|
||||
# Allow to write contents to push commits
|
||||
@@ -22,25 +39,37 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Update latest Flutter version
|
||||
id: update_flutter_version
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const script = require('./script/updateFlutterVersion.js')
|
||||
return await script({core, fetch})
|
||||
- name: Setup mise tools
|
||||
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
|
||||
|
||||
- name: Setup CUE
|
||||
if: ${{ steps.update_flutter_version.outputs.result == 'true' }}
|
||||
uses: jaxxstorm/action-install-gh-release@6096f2a2bbfee498ced520b6922ac2c06e990ed2 # v2.1.0
|
||||
with:
|
||||
repo: cue-lang/cue
|
||||
tag: v0.15.0
|
||||
digest: 06925fc1e5174591cef0b1e42ac32cff4271804742cd20893de1793b6d82d460
|
||||
- name: Fetch and update latest Flutter version
|
||||
id: update_flutter_version
|
||||
run: |
|
||||
releases_json_path="${RUNNER_TEMP}/releases.json"
|
||||
curl -fsSL https://storage.googleapis.com/flutter_infra_release/releases/releases_linux.json \
|
||||
-o "$releases_json_path"
|
||||
|
||||
old_version=$(jq -r '.flutter.version' "${{ env.FLUTTER_VERSION_PATH }}")
|
||||
new_version=$(jq -r '[.releases[] | select(.channel=="stable")] | max_by(.release_date) | .version' "$releases_json_path")
|
||||
new_channel=$(jq -r '[.releases[] | select(.channel=="stable")] | max_by(.release_date) | .channel' "$releases_json_path")
|
||||
new_commit=$(jq -r '[.releases[] | select(.channel=="stable")] | max_by(.release_date) | .hash' "$releases_json_path")
|
||||
|
||||
echo "Current pinned version: $old_version"
|
||||
echo "Latest upstream stable version: $new_version (channel: $new_channel)"
|
||||
|
||||
if [ "$old_version" = "$new_version" ]; then
|
||||
echo "No version change — skipping update."
|
||||
echo "result=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "New version detected — updating ${{ env.FLUTTER_VERSION_PATH }}"
|
||||
printf '{\n "flutter": {\n "channel": "%s",\n "commit": "%s",\n "version": "%s"\n }\n}\n' \
|
||||
"$new_channel" "$new_commit" "$new_version" > "${{ env.FLUTTER_VERSION_PATH }}"
|
||||
echo "result=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Validate version.json with CUE
|
||||
if: ${{ steps.update_flutter_version.outputs.result == 'true' }}
|
||||
run: cue vet config/version.cue -d '#FlutterVersion' config/flutter_version.json
|
||||
run: cue vet config/schema.cue -d '#FlutterVersion' ${{ env.FLUTTER_VERSION_PATH }}
|
||||
|
||||
- name: Upload artifact with the new Flutter version
|
||||
if: ${{ steps.update_flutter_version.outputs.result == 'true' }}
|
||||
@@ -48,7 +77,116 @@ jobs:
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: flutter_version.json
|
||||
path: config/flutter_version.json
|
||||
path: ${{ env.FLUTTER_VERSION_PATH }}
|
||||
|
||||
update_windows_version:
|
||||
permissions:
|
||||
contents: write
|
||||
needs: update_flutter_version
|
||||
if: ${{ needs.update_flutter_version.outputs.new_version == 'true' }}
|
||||
outputs:
|
||||
version_artifact_id: ${{ steps.upload-version.outputs.artifact-id }}
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup mise tools
|
||||
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
|
||||
|
||||
# Parallel pattern with update_android_version: the artifact is downloaded
|
||||
# to keep job structure symmetric, even though windows fields are not
|
||||
# currently tied to a specific Flutter tag.
|
||||
- name: Delete flutter_version.json
|
||||
run: rm ${{ env.FLUTTER_VERSION_PATH }}
|
||||
|
||||
- name: Download artifact with the new Flutter version
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
artifact-ids: ${{ needs.update_flutter_version.outputs.flutter_version_artifact_id }}
|
||||
path: config
|
||||
|
||||
- name: Resolve latest Git for Windows version
|
||||
id: git_version
|
||||
run: |
|
||||
tag=$(curl -fsSL https://api.github.com/repos/git-for-windows/git/releases/latest | jq -r '.tag_name')
|
||||
version=${tag#v}
|
||||
version=${version%.windows.*}
|
||||
echo "git_version=$version" >> "$GITHUB_OUTPUT"
|
||||
echo "Resolved Git for Windows version: $version"
|
||||
|
||||
- name: Fetch VS channel manifest
|
||||
run: curl -fsSL https://aka.ms/vs/17/release/channel -o channel.json
|
||||
|
||||
# The channel manifest only contains product-level entries; per-component
|
||||
# versions live in VisualStudio.vsman, referenced and SHA-256 pinned from
|
||||
# the channel via the Microsoft.VisualStudio.Manifests.VisualStudio item.
|
||||
- name: Resolve VS catalog manifest payload
|
||||
id: vsman_payload
|
||||
run: |
|
||||
url=$(jq -r '.channelItems[] | select(.id=="Microsoft.VisualStudio.Manifests.VisualStudio") | .payloads[0].url' channel.json)
|
||||
sha=$(jq -r '.channelItems[] | select(.id=="Microsoft.VisualStudio.Manifests.VisualStudio") | .payloads[0].sha256' channel.json)
|
||||
echo "url=$url" >> "$GITHUB_OUTPUT"
|
||||
echo "sha=$sha" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Download and verify VS catalog manifest
|
||||
run: |
|
||||
curl -fsSL "${{ steps.vsman_payload.outputs.url }}" -o vsman.json
|
||||
echo "${{ steps.vsman_payload.outputs.sha }} vsman.json" | sha256sum -c -
|
||||
|
||||
- name: Resolve VS BuildTools component versions
|
||||
id: vs_versions
|
||||
run: |
|
||||
cmake=$(jq -r '.packages[] | select(.id=="Microsoft.VisualStudio.Component.VC.CMake.Project") | .version' vsman.json)
|
||||
vctools=$(jq -r '.packages[] | select(.id=="Microsoft.VisualStudio.Workload.VCTools") | .version' vsman.json)
|
||||
echo "cmake=$cmake" >> "$GITHUB_OUTPUT"
|
||||
echo "vctools=$vctools" >> "$GITHUB_OUTPUT"
|
||||
echo "CMake: $cmake; VCTools: $vctools"
|
||||
|
||||
- name: Write windows block into config/version.json
|
||||
env:
|
||||
GIT_VERSION: ${{ steps.git_version.outputs.git_version }}
|
||||
VS_CMAKE_VERSION: ${{ steps.vs_versions.outputs.cmake }}
|
||||
VS_VCTOOLS_VERSION: ${{ steps.vs_versions.outputs.vctools }}
|
||||
run: |
|
||||
# Win11SDK build id is human-pinned (changes infrequently; tied to OS branding).
|
||||
win11sdk_build=$(jq -r '.windows.vsBuildTools.windows11Sdk.build' config/version.json)
|
||||
jq --arg git "$GIT_VERSION" \
|
||||
--arg cmake "$VS_CMAKE_VERSION" \
|
||||
--arg vctools "$VS_VCTOOLS_VERSION" \
|
||||
--argjson sdkbuild "$win11sdk_build" \
|
||||
'.windows = {
|
||||
git: {version: $git},
|
||||
vsBuildTools: {
|
||||
cmakeProject: {version: $cmake},
|
||||
windows11Sdk: {build: $sdkbuild},
|
||||
vcTools: {version: $vctools}
|
||||
}
|
||||
}' config/version.json > config/version.json.tmp
|
||||
mv config/version.json.tmp config/version.json
|
||||
|
||||
- name: Validate version.json with CUE
|
||||
run: cue vet config/schema.cue -d '#Version' config/version.json
|
||||
|
||||
# The artifact's file is renamed so it doesn't collide with the Android
|
||||
# artifact's version.json in update_docs_and_create_pr's merge step.
|
||||
- name: Stage windows-only artifact
|
||||
run: cp config/version.json "${RUNNER_TEMP}/version.json.windows"
|
||||
|
||||
- name: Upload artifact with the updated windows block
|
||||
id: upload-version
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: version.json.windows
|
||||
path: ${{ runner.temp }}/version.json.windows
|
||||
|
||||
- name: Upload VS manifest artifacts for forensics
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: vs-manifests
|
||||
path: |
|
||||
channel.json
|
||||
vsman.json
|
||||
|
||||
update_android_version:
|
||||
permissions:
|
||||
@@ -58,14 +196,14 @@ jobs:
|
||||
packages: read
|
||||
# Allow to write pull requests to create a pull request
|
||||
pull-requests: write
|
||||
needs: update_flutter_version
|
||||
needs: [setup, update_flutter_version]
|
||||
if: ${{ needs.update_flutter_version.outputs.new_version == 'true' }}
|
||||
outputs:
|
||||
version_artifact_id: ${{ steps.upload-version.outputs.artifact-id }}
|
||||
android_test_artifact_id: ${{ steps.upload-android-test.outputs.artifact-id }}
|
||||
runs-on: ubuntu-24.04
|
||||
container:
|
||||
image: ghcr.io/${{ github.repository_owner }}/flutter-android:${{ vars.FLUTTER_VERSION }}
|
||||
image: ghcr.io/${{ github.repository_owner }}/flutter-android:${{ needs.setup.outputs.flutter_version }}
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
@@ -78,7 +216,7 @@ jobs:
|
||||
# https://github.com/actions/download-artifact/issues/225
|
||||
# https://github.com/actions/download-artifact/issues/138
|
||||
- name: Delete flutter_version.json
|
||||
run: rm config/flutter_version.json
|
||||
run: rm ${{ env.FLUTTER_VERSION_PATH }}
|
||||
|
||||
- name: Download artifact with the new Flutter version
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
@@ -100,6 +238,13 @@ jobs:
|
||||
const script = require('./script/updateFastlaneVersion.js')
|
||||
await script({core, fetch})
|
||||
|
||||
- name: Update Android SDK build tools version
|
||||
id: build_tools
|
||||
run: |
|
||||
build_tools_version=$(curl -fsSL https://raw.githubusercontent.com/flutter/flutter/refs/tags/${{ env.FLUTTER_VERSION }}/engine/src/flutter/tools/android_sdk/packages.txt | grep '^build-tools' | awk -F'[;,:]' '{print $2}')
|
||||
echo "build_tools_version=$build_tools_version" >>"$GITHUB_OUTPUT"
|
||||
echo "Build tools version: $build_tools_version"
|
||||
|
||||
- name: Setup Flutter
|
||||
run: |
|
||||
cd $FLUTTER_ROOT
|
||||
@@ -114,6 +259,8 @@ jobs:
|
||||
# TODO: Cache gradle https://github.com/gradle/gradle-build-action
|
||||
- name: Update default Android platform versions in Flutter
|
||||
working-directory: test_app/android
|
||||
env:
|
||||
BUILD_TOOLS_VERSION: ${{ steps.build_tools.outputs.build_tools_version }}
|
||||
run: |
|
||||
cat ../../script/updateAndroidVersions.gradle.kts >> app/build.gradle.kts
|
||||
./gradlew --warning-mode all updateAndroidVersions
|
||||
@@ -122,17 +269,16 @@ jobs:
|
||||
run: |
|
||||
rm -rf test_app
|
||||
|
||||
- name: Setup CUE
|
||||
uses: jaxxstorm/action-install-gh-release@6096f2a2bbfee498ced520b6922ac2c06e990ed2 # v2.1.0
|
||||
with:
|
||||
repo: cue-lang/cue
|
||||
tag: v0.15.0
|
||||
digest: 06925fc1e5174591cef0b1e42ac32cff4271804742cd20893de1793b6d82d460
|
||||
- name: Setup mise tools
|
||||
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
|
||||
|
||||
- name: Generate test files with CUE
|
||||
run: |
|
||||
./script/update_test.sh
|
||||
|
||||
- name: Validate version.json with CUE
|
||||
run: cue vet config/schema.cue -d '#Version' config/version.json
|
||||
|
||||
- name: Upload artifact with the updated version.json
|
||||
id: upload-version
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
@@ -167,20 +313,17 @@ jobs:
|
||||
artifact-ids: ${{ needs.update_android_version.outputs.version_artifact_id }}
|
||||
path: config
|
||||
|
||||
- name: Setup CUE
|
||||
uses: jaxxstorm/action-install-gh-release@6096f2a2bbfee498ced520b6922ac2c06e990ed2 # v2.1.0
|
||||
with:
|
||||
repo: cue-lang/cue
|
||||
tag: v0.15.0
|
||||
digest: 06925fc1e5174591cef0b1e42ac32cff4271804742cd20893de1793b6d82d460
|
||||
- name: Setup mise tools
|
||||
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
|
||||
|
||||
- name: Validate version.json with CUE
|
||||
run: cue vet config/version.cue -d '#Version' config/version.json
|
||||
run: cue vet config/schema.cue -d '#Version' config/version.json
|
||||
|
||||
update_docs_and_create_pr:
|
||||
needs:
|
||||
- update_flutter_version
|
||||
- update_android_version
|
||||
- update_windows_version
|
||||
- validate_config_version
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
@@ -196,34 +339,48 @@ jobs:
|
||||
# https://github.com/actions/download-artifact/issues/138
|
||||
- name: Delete flutter_version.json and version.json
|
||||
run: |-
|
||||
rm config/flutter_version.json config/version.json test/android.yml
|
||||
rm ${{ env.FLUTTER_VERSION_PATH }} config/version.json test/android.yml
|
||||
|
||||
- name: Download configuration artifacts
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
artifact-ids: ${{ needs.update_flutter_version.outputs.flutter_version_artifact_id }},${{ needs.update_android_version.outputs.version_artifact_id }}
|
||||
artifact-ids: ${{ needs.update_flutter_version.outputs.flutter_version_artifact_id }},${{ needs.update_android_version.outputs.version_artifact_id }},${{ needs.update_windows_version.outputs.version_artifact_id }}
|
||||
path: config
|
||||
# Download to the configured path instead of separated directories by artifact id
|
||||
# Download to the configured path instead of separated directories by artifact id.
|
||||
# Artifacts contain distinct filenames (flutter_version.json, version.json,
|
||||
# version.json.windows) so merge-multiple is safe.
|
||||
merge-multiple: true
|
||||
|
||||
# Merge order: the Android artifact's version.json is the base (it carries
|
||||
# the new flutter and android blocks). The Windows artifact's windows block
|
||||
# overlays the windows field. This keeps each producer authoritative over
|
||||
# its own block.
|
||||
- name: Merge windows block into version.json
|
||||
run: |
|
||||
jq -s '.[0] + {windows: .[1].windows}' config/version.json config/version.json.windows > config/version.json.merged
|
||||
mv config/version.json.merged config/version.json
|
||||
rm config/version.json.windows
|
||||
|
||||
- name: Setup mise tools
|
||||
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
|
||||
|
||||
- name: Validate merged version.json with CUE
|
||||
run: cue vet config/schema.cue -d '#Version' config/version.json
|
||||
|
||||
- name: Download test artifacts
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
artifact-ids: ${{ needs.update_android_version.outputs.android_test_artifact_id }}
|
||||
path: test
|
||||
|
||||
- name: Setup NodeJS
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
cache: npm
|
||||
cache-dependency-path: docs/src/package-lock.json
|
||||
node-version: lts/*
|
||||
- name: Setup mise tools
|
||||
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
|
||||
|
||||
- name: Update documentation
|
||||
working-directory: docs/src
|
||||
run: |
|
||||
npm ci --prefer-offline
|
||||
npm run build
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm run build
|
||||
|
||||
- name: Read environment variables from the version manifest
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
@@ -237,8 +394,11 @@ jobs:
|
||||
return await script({ core })
|
||||
|
||||
- name: Create commit message variable
|
||||
id: create_commit_message
|
||||
run: |
|
||||
echo "COMMIT_MESSAGE=chore(release): upgrade flutter to ${{ env.FLUTTER_VERSION }}" >> $GITHUB_ENV
|
||||
commit_message="chore(release): upgrade flutter to ${{ env.FLUTTER_VERSION }}"
|
||||
echo "commit_message=$commit_message" >> "$GITHUB_OUTPUT"
|
||||
echo "Commit message: $commit_message"
|
||||
|
||||
- name: Generate authentication token with GitHub App to trigger Actions
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
@@ -253,8 +413,8 @@ jobs:
|
||||
- name: Create pull request if there are changes
|
||||
uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11
|
||||
with:
|
||||
commit-message: ${{ env.COMMIT_MESSAGE }}
|
||||
commit-message: ${{ steps.create_commit_message.outputs.commit_message }}
|
||||
branch: update-flutter-dependencies/${{ env.FLUTTER_VERSION }}
|
||||
sign-commits: true
|
||||
title: ${{ env.COMMIT_MESSAGE }}
|
||||
title: ${{ steps.create_commit_message.outputs.commit_message }}
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
@@ -12,22 +12,22 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
test_windows:
|
||||
permissions:
|
||||
# Allow to write packages for the docker/scout-action to write a comment
|
||||
packages: write
|
||||
# Allow to write pull requests for the docker/scout-action to write a comment
|
||||
pull-requests: write
|
||||
# Allow to write security events for github/codeql-action/upload-sarif to upload SARIF results
|
||||
security-events: write
|
||||
runs-on: windows-2025
|
||||
env:
|
||||
IMAGE_REPOSITORY_NAME: flutter-android
|
||||
IMAGE_REPOSITORY_NAME: flutter-windows
|
||||
VERSION_MANIFEST: config/version.json
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Clean runner disk
|
||||
uses: ./.github/actions/clean-runner-disk
|
||||
|
||||
# Secrets are not available to PRs from forks. The Windows base image
|
||||
# comes from mcr.microsoft.com, so Docker Hub auth is not strictly
|
||||
# required to build/test the image.
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
@@ -44,56 +44,27 @@ jobs:
|
||||
const script = require('./script/setEnvironmentVariables.js')
|
||||
return await script({ core })
|
||||
|
||||
# - name: Load image metadata
|
||||
# uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
# id: metadata
|
||||
# with:
|
||||
# images: |
|
||||
# ${{ env.IMAGE_REPOSITORY_PATH }}
|
||||
# tags: |
|
||||
# type=raw,value=${{ env.FLUTTER_VERSION }}
|
||||
- name: Load image metadata
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
id: metadata
|
||||
with:
|
||||
images: |
|
||||
${{ env.IMAGE_REPOSITORY_PATH }}
|
||||
tags: |
|
||||
type=raw,value=${{ env.FLUTTER_VERSION }}
|
||||
|
||||
# - name: Set up Docker Buildx
|
||||
# uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||
|
||||
- name: Build image and push to local Docker daemon
|
||||
# Docker Buildx is not supported for Windows containers
|
||||
# so we'll use direct docker build commands
|
||||
- name: Test image and push to local Docker daemon
|
||||
shell: powershell
|
||||
run: |
|
||||
docker build . -f windows.Dockerfile --build-arg flutter_version=${{ env.FLUTTER_VERSION }} -t ${{ env.IMAGE_REPOSITORY_PATH }}
|
||||
docker build . -f windows.Dockerfile `
|
||||
--build-arg flutter_version=${{ env.FLUTTER_VERSION }} `
|
||||
--build-arg git_version=${{ env.GIT_VERSION }} `
|
||||
--build-arg vs_cmake_version=${{ env.VS_CMAKE_VERSION }} `
|
||||
--build-arg vs_win11sdk_build=${{ env.VS_WIN11SDK_BUILD }} `
|
||||
--build-arg vs_vctools_version=${{ env.VS_VCTOOLS_VERSION }} `
|
||||
-t ${{ fromJson(steps.metadata.outputs.json).tags[0] }} --target test
|
||||
|
||||
# - name: Build image and push to local Docker daemon
|
||||
# uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
|
||||
# with:
|
||||
# file: windows.Dockerfile
|
||||
# load: true
|
||||
# cache-from: type=gha
|
||||
# cache-to: type=gha,mode=max
|
||||
# labels: ${{ steps.metadata.outputs.labels }}
|
||||
# tags: ${{ steps.metadata.outputs.tags }}
|
||||
# target: android
|
||||
# build-args: |
|
||||
# flutter_version=${{ env.FLUTTER_VERSION }}
|
||||
docker run --rm ${{ fromJson(steps.metadata.outputs.json).tags[0] }}
|
||||
|
||||
# - name: Test image
|
||||
# uses: plexsystems/container-structure-test-action@c0a028aa96e8e82ae35be556040340cbb3e280ca # v0.3.0
|
||||
# with:
|
||||
# image: ${{ fromJSON(steps.metadata.outputs.json).tags[0] }}
|
||||
# config: test/android.yml
|
||||
|
||||
# # TODO: Parallelize testing and vulnerability scanning
|
||||
# - name: Scan with Docker Scout
|
||||
# id: docker-scout
|
||||
# uses: docker/scout-action@0133ff88fe16d4a412dc4827a8fccbccb6b583e0 # v1.16.3
|
||||
# with:
|
||||
# command: compare, recommendations
|
||||
# # Use the Docker Hub image that is the first tag in the metadata
|
||||
# image: local://${{ fromJson(steps.metadata.outputs.json).tags[0] }}
|
||||
# # github-token is needed to be able to write the PR comment
|
||||
# github-token: ${{ github.token }}
|
||||
# only-fixed: true
|
||||
# organization: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
# # sarif-file: output.sarif.json
|
||||
# to-env: prod
|
||||
# # Enable debug logging when needed
|
||||
# # debug: true
|
||||
# # verbose-debug: true
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
/.env
|
||||
/test_app
|
||||
/.vscode
|
||||
/.vscode
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
FROM debian:13.2-slim@sha256:4bcb9db66237237d03b55b969271728dd3d955eaaa254b9db8a3db94550b1885 AS flutter
|
||||
FROM debian:13.4-slim@sha256:109e2c65005bf160609e4ba6acf7783752f8502ad218e298253428690b9eaa4b AS flutter
|
||||
|
||||
SHELL ["/bin/bash", "-euxo", "pipefail", "-c"]
|
||||
|
||||
@@ -150,9 +150,9 @@ ENV ANDROID_HOME="$SDK_ROOT/android-sdk" \
|
||||
ENV PATH="$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$HOME/.local/bin"
|
||||
|
||||
# renovate: suite=bookworm depName=openjdk-17-jdk-headless
|
||||
ARG OPENJDK_17_JDK_HEADLESS_VERSION="17.0.18+8-1~deb12u1"
|
||||
ARG OPENJDK_17_JDK_HEADLESS_VERSION="17.0.19+10-1~deb12u2"
|
||||
# renovate: suite=trixie depName=sudo
|
||||
ARG SUDO_VERSION="1.9.16p2-3"
|
||||
ARG SUDO_VERSION="1.9.16p2-3+deb13u1"
|
||||
|
||||
USER root
|
||||
# Add debian 12 bookworm repository alongside debian 13 trixie to install Java 17
|
||||
|
||||
@@ -1,3 +1,73 @@
|
||||
## [3.41.9] - 2026-05-22
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- *(ci)* Adopt gx for GitHub Actions version tracking (#439)
|
||||
- *(renovate)* Manage GitHub Actions through gx.toml (#441)
|
||||
- *(release)* Publish flutter-windows image; archive p1 (#443)
|
||||
- Implement p2 image handoff (#451)
|
||||
- Pin Windows toolchain versions in config/version.json (#456)
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Add openspec config (#440)
|
||||
- Update windows docs (#442)
|
||||
- Add DeepWiki integration (#444)
|
||||
- *(openspec)* Add p2-p5 CI speedup proposals (#450)
|
||||
- Archive p2-image-handoff-via-pr-tag (#452)
|
||||
- Archive p2-release-windows-image (#455)
|
||||
- Archive p3-parallelize-image-validation (#454)
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Add openspec Claude Code skills and commands (#438)
|
||||
- Update version.json from a single place (#375)
|
||||
- Test windows image (#339)
|
||||
- Skip secret-dependent steps on fork PRs (#446)
|
||||
- Update fastlane version to 2.234.0 (#445)
|
||||
- Make workflows fork-PR friendly by removing vars.FLUTTER_VERSION (#447)
|
||||
- Unify runner disk cleanup across Linux and Windows (#448)
|
||||
- Switch build cache to GHCR registry backend (#449)
|
||||
- Parallelize image validation (#453)
|
||||
## [3.41.9] - 2026-05-22
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(release)* Upgrade flutter to 3.41.9 (#437)
|
||||
## [3.41.8] - 2026-05-01
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(release)* Upgrade flutter to 3.41.8 (#436)
|
||||
## [3.41.7] - 2026-05-01
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(deps)* Bump yaml from 2.8.2 to 2.8.3 (#432)
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(release)* Upgrade flutter to 3.41.7 (#434)
|
||||
## [3.41.6] - 2026-03-27
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(release)* Upgrade flutter to 3.41.6 (#431)
|
||||
## [3.41.5] - 2026-03-21
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(release)* Upgrade flutter to 3.41.5 (#430)
|
||||
## [3.41.4] - 2026-03-19
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(release)* Upgrade flutter to 3.41.4 (#429)
|
||||
## [3.41.2] - 2026-02-20
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(release)* Upgrade flutter to 3.41.2 (#427)
|
||||
## [3.41.1] - 2026-02-16
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
+17
-15
@@ -36,25 +36,27 @@ output: {
|
||||
|
||||
commandTests: list.Concat([
|
||||
list.Take(input.commandTests, 1),
|
||||
[
|
||||
{
|
||||
name: input.commandTests[1].name
|
||||
command: input.commandTests[1].command
|
||||
args: input.commandTests[1].args
|
||||
expectedOutput: [android_sdk_build_tools_version]
|
||||
},
|
||||
{
|
||||
name: input.commandTests[2].name
|
||||
command: input.commandTests[2].command
|
||||
args: input.commandTests[2].args
|
||||
expectedOutput: [android_ndk_version]
|
||||
}
|
||||
],
|
||||
if len(input.commandTests) >= 3 {
|
||||
[
|
||||
{
|
||||
name: input.commandTests[1].name
|
||||
command: input.commandTests[1].command
|
||||
args: input.commandTests[1].args
|
||||
expectedOutput: [android_sdk_build_tools_version]
|
||||
},
|
||||
{
|
||||
name: input.commandTests[2].name
|
||||
command: input.commandTests[2].command
|
||||
args: input.commandTests[2].args
|
||||
expectedOutput: [android_ndk_version]
|
||||
}
|
||||
]
|
||||
},
|
||||
list.Drop(input.commandTests, 3),
|
||||
])
|
||||
|
||||
fileContentTests: list.Concat([
|
||||
if len(input.fileContentTests) > 0 {
|
||||
if len(input.fileContentTests) >= 1 {
|
||||
[{
|
||||
name: "Android SDK Command-line Tools is version \(android_cmdline_tools_version)"
|
||||
path: input.fileContentTests[0].path
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import "strings"
|
||||
import "list"
|
||||
|
||||
#Version3: {
|
||||
version!: =~ "^\\d+.\\d+.\\d+$"
|
||||
}
|
||||
|
||||
flutter: {
|
||||
channel!: "stable" | "beta"
|
||||
commit!: strings.MaxRunes(40)
|
||||
#Version3
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"flutter": {
|
||||
"channel": "stable",
|
||||
"commit": "582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536",
|
||||
"version": "3.41.1"
|
||||
"commit": "00b0c91f06209d9e4a41f71b7a512d6eb3b9c694",
|
||||
"version": "3.41.9"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import "strings"
|
||||
import "list"
|
||||
|
||||
#PlatformVersion: {
|
||||
version!: int
|
||||
}
|
||||
|
||||
#SemverMinor: {
|
||||
version!: =~ "^\\d+.\\d+$"
|
||||
}
|
||||
|
||||
#SemverPatch: {
|
||||
version!: =~ "^\\d+.\\d+.\\d+$"
|
||||
}
|
||||
|
||||
#SemverQuad: {
|
||||
version!: =~ "^\\d+\\.\\d+\\.\\d+\\.\\d+$"
|
||||
}
|
||||
|
||||
#WindowsToolchain: {
|
||||
git: #SemverPatch
|
||||
vsBuildTools: {
|
||||
cmakeProject: #SemverQuad
|
||||
windows11Sdk: {
|
||||
build!: int
|
||||
}
|
||||
vcTools: #SemverQuad
|
||||
}
|
||||
}
|
||||
|
||||
#FlutterVersion: {
|
||||
flutter: {
|
||||
channel!: "stable"
|
||||
commit!: strings.MaxRunes(40)
|
||||
#SemverPatch
|
||||
}
|
||||
}
|
||||
|
||||
#SemverVersion: #SemverMinor | #SemverPatch
|
||||
|
||||
#Version: {
|
||||
#FlutterVersion
|
||||
|
||||
android: {
|
||||
platforms!: [...#PlatformVersion] & list.MinItems(1) & list.UniqueItems
|
||||
gradle!: #SemverVersion
|
||||
buildTools!: #SemverPatch
|
||||
cmdlineTools!: #SemverMinor
|
||||
ndk!: #SemverPatch
|
||||
cmake!: #SemverPatch
|
||||
}
|
||||
|
||||
fastlane!: #SemverPatch
|
||||
|
||||
windows!: #WindowsToolchain
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import "strings"
|
||||
import "list"
|
||||
|
||||
#PlatformVersion: {
|
||||
version!: int
|
||||
}
|
||||
|
||||
#MinorVersion: {
|
||||
version!: =~ "^\\d+.\\d+$"
|
||||
}
|
||||
|
||||
#PatchVersion: {
|
||||
version!: =~ "^\\d+.\\d+.\\d+$"
|
||||
}
|
||||
|
||||
#FlutterVersion: {
|
||||
flutter: {
|
||||
channel!: "stable" | "beta"
|
||||
commit!: strings.MaxRunes(40)
|
||||
#PatchVersion
|
||||
}
|
||||
}
|
||||
|
||||
#MinorOrPatchVersion: #MinorVersion | #PatchVersion
|
||||
|
||||
#Version: {
|
||||
#FlutterVersion
|
||||
|
||||
android: {
|
||||
platforms!: [...#PlatformVersion] & list.MinItems(1) & list.UniqueItems
|
||||
gradle!: #MinorOrPatchVersion
|
||||
buildTools!: #PatchVersion
|
||||
cmdlineTools!: #MinorVersion
|
||||
ndk!: #PatchVersion
|
||||
cmake!: #PatchVersion
|
||||
}
|
||||
|
||||
fastlane!: #PatchVersion
|
||||
}
|
||||
+19
-3
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"flutter": {
|
||||
"channel": "stable",
|
||||
"commit": "582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536",
|
||||
"version": "3.41.1"
|
||||
"commit": "00b0c91f06209d9e4a41f71b7a512d6eb3b9c694",
|
||||
"version": "3.41.9"
|
||||
},
|
||||
"android": {
|
||||
"platforms": [
|
||||
@@ -27,6 +27,22 @@
|
||||
}
|
||||
},
|
||||
"fastlane": {
|
||||
"version": "2.232.1"
|
||||
"version": "2.234.0"
|
||||
},
|
||||
"windows": {
|
||||
"git": {
|
||||
"version": "2.46.0"
|
||||
},
|
||||
"vsBuildTools": {
|
||||
"cmakeProject": {
|
||||
"version": "17.14.36510.44"
|
||||
},
|
||||
"windows11Sdk": {
|
||||
"build": 22621
|
||||
},
|
||||
"vcTools": {
|
||||
"version": "17.14.36331.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,5 +34,13 @@ services:
|
||||
windows:
|
||||
build:
|
||||
dockerfile: ./windows.Dockerfile
|
||||
target: flutter
|
||||
args:
|
||||
flutter_version: $FLUTTER_VERSION
|
||||
|
||||
windows-test:
|
||||
build:
|
||||
dockerfile: ./windows.Dockerfile
|
||||
target: test
|
||||
args:
|
||||
flutter_version: $FLUTTER_VERSION
|
||||
@@ -2,6 +2,46 @@
|
||||
|
||||
# Contributing
|
||||
|
||||
## Repository wiki
|
||||
|
||||
An AI-generated wiki for this repository is available at [deepwiki.com/gmeligio/flutter-docker-image](https://deepwiki.com/gmeligio/flutter-docker-image). It covers architecture, Dockerfile stages, the CI/release pipeline, and more.
|
||||
|
||||
The wiki is kept current automatically: DeepWiki re-indexes the repository whenever it detects the DeepWiki badge in `readme.md`. No manual action is required after merging to `main`.
|
||||
|
||||
## Building the docs locally
|
||||
|
||||
The Markdown files at the repository root (`readme.md`, `LICENSE.md`, `docs/contributing.md`, `docs/windows.md`) are generated from the MDX sources under `docs/src/`. The build uses `pnpm`, pinned in `mise.toml` alongside the other CI tools. From `docs/src/`:
|
||||
|
||||
```bash
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm run build
|
||||
|
||||
```
|
||||
|
||||
`devEngines.packageManager` in `docs/src/package.json` is set to `pnpm` with `onFail: "error"`, so `npm install` will refuse to run.
|
||||
|
||||
## Editing GitHub Actions workflows
|
||||
|
||||
GitHub Actions versions are tracked with [gx](https://github.com/gmeligio/gx). The manifest at `.github/gx.toml` is the source of truth for version constraints, and `.github/gx.lock` records the resolved SHAs. Workflows must use SHA pins with a `# vX.Y.Z` comment.
|
||||
|
||||
When editing any file under `.github/workflows/` or `.github/actions/`:
|
||||
|
||||
1. Install `gx` locally: `brew install gmeligio/tap/gx`, `cargo install gx`, or grab a binary from [the GitHub Releases page](https://github.com/gmeligio/gx/releases).
|
||||
2. Make your workflow edits.
|
||||
3. Run `gx tidy` to sync `.github/gx.toml`, `.github/gx.lock`, and the workflow `uses:` lines.
|
||||
4. Commit all three together (`.github/workflows/...`, `.github/gx.toml`, `.github/gx.lock`).
|
||||
|
||||
Adding a new action looks like adding a single line under `[actions]` in `.github/gx.toml`:
|
||||
|
||||
```toml
|
||||
"some-org/some-action" = "^1"
|
||||
|
||||
```
|
||||
|
||||
…then `gx tidy` resolves the SHA, writes the lock entry, and rewrites the `uses:` line.
|
||||
|
||||
The `lint` job in `.github/workflows/gx.yml` fails any pull request that introduces an unpinned `uses:` reference or a lock that disagrees with the workflows. The sibling `tidy` job runs `gx tidy` and pushes a fixup commit on PRs from this repository if the lock is stale (forks must run `gx tidy` locally).
|
||||
|
||||
## Adding new Github Actions
|
||||
|
||||
When adding new Github Actions the `.github\renovate.json` needs to be checked and add the new action to:
|
||||
|
||||
+1
-1
@@ -8,6 +8,6 @@ export const channelColor = {
|
||||
export const channelUrl = 'https://docs.flutter.dev/release/archive?tab=linux';
|
||||
export const channelBadgeUrl = `https://img.shields.io/static/v1?label=${encodeURIComponent('channel')}&message=${encodeURIComponent(flutterChannel)}&color=${encodeURIComponent(channelColor[flutterChannel])}`;
|
||||
|
||||
[](https://scorecard.dev/viewer/?uri=github.com/gmeligio/flutter-docker-image)
|
||||
[](https://scorecard.dev/viewer/?uri=github.com/gmeligio/flutter-docker-image) [](https://deepwiki.com/gmeligio/flutter-docker-image)
|
||||
<a href={channelUrl}><img src={channelBadgeUrl} alt="channel" /></a> [](https://hub.docker.com/r/gmeligio/flutter-android/tags)
|
||||
[](https://hub.docker.com/r/gmeligio/flutter-android/tags)
|
||||
|
||||
+13
-10
@@ -18,14 +18,17 @@ function mdxOptions(options) {
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
const sourceRelativePath = args[0]
|
||||
const outputRelativePath = args[1]
|
||||
const markdown = await mdxToMd(resolve(sourceRelativePath), {
|
||||
mdxOptions,
|
||||
})
|
||||
const banner = `This markdown file was auto-generated from "${sourceRelativePath}"`
|
||||
const readme = `<!--- ${banner} -->\n\n${markdown}`
|
||||
if (args.length === 0 || args.length % 2 !== 0) {
|
||||
console.error('Usage: node compile.js <src.mdx> <dst.md> [<src.mdx> <dst.md> ...]')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await writeFile(outputRelativePath, readme)
|
||||
|
||||
console.log(`📝 Converted ${sourceRelativePath} -> ${outputRelativePath}`)
|
||||
for (let i = 0; i < args.length; i += 2) {
|
||||
const sourceRelativePath = args[i]
|
||||
const outputRelativePath = args[i + 1]
|
||||
const markdown = await mdxToMd(resolve(sourceRelativePath), { mdxOptions })
|
||||
const banner = `This markdown file was auto-generated from "${sourceRelativePath}"`
|
||||
const output = `<!--- ${banner} -->\n\n${markdown}`
|
||||
await writeFile(outputRelativePath, output)
|
||||
console.log(`📝 Converted ${sourceRelativePath} -> ${outputRelativePath}`)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,43 @@
|
||||
# Contributing
|
||||
|
||||
## Repository wiki
|
||||
|
||||
An AI-generated wiki for this repository is available at [deepwiki.com/gmeligio/flutter-docker-image](https://deepwiki.com/gmeligio/flutter-docker-image). It covers architecture, Dockerfile stages, the CI/release pipeline, and more.
|
||||
|
||||
The wiki is kept current automatically: DeepWiki re-indexes the repository whenever it detects the DeepWiki badge in `readme.md`. No manual action is required after merging to `main`.
|
||||
|
||||
## Building the docs locally
|
||||
|
||||
The Markdown files at the repository root (`readme.md`, `LICENSE.md`, `docs/contributing.md`, `docs/windows.md`) are generated from the MDX sources under `docs/src/`. The build uses `pnpm`, pinned in `mise.toml` alongside the other CI tools. From `docs/src/`:
|
||||
|
||||
```bash
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
`devEngines.packageManager` in `docs/src/package.json` is set to `pnpm` with `onFail: "error"`, so `npm install` will refuse to run.
|
||||
|
||||
## Editing GitHub Actions workflows
|
||||
|
||||
GitHub Actions versions are tracked with [`gx`](https://github.com/gmeligio/gx). The manifest at `.github/gx.toml` is the source of truth for version constraints, and `.github/gx.lock` records the resolved SHAs. Workflows must use SHA pins with a `# vX.Y.Z` comment.
|
||||
|
||||
When editing any file under `.github/workflows/` or `.github/actions/`:
|
||||
|
||||
1. Install `gx` locally: `brew install gmeligio/tap/gx`, `cargo install gx`, or grab a binary from [the GitHub Releases page](https://github.com/gmeligio/gx/releases).
|
||||
2. Make your workflow edits.
|
||||
3. Run `gx tidy` to sync `.github/gx.toml`, `.github/gx.lock`, and the workflow `uses:` lines.
|
||||
4. Commit all three together (`.github/workflows/...`, `.github/gx.toml`, `.github/gx.lock`).
|
||||
|
||||
Adding a new action looks like adding a single line under `[actions]` in `.github/gx.toml`:
|
||||
|
||||
```toml
|
||||
"some-org/some-action" = "^1"
|
||||
```
|
||||
|
||||
…then `gx tidy` resolves the SHA, writes the lock entry, and rewrites the `uses:` line.
|
||||
|
||||
The `lint` job in `.github/workflows/gx.yml` fails any pull request that introduces an unpinned `uses:` reference or a lock that disagrees with the workflows. The sibling `tidy` job runs `gx tidy` and pushes a fixup commit on PRs from this repository if the lock is stale (forks must run `gx tidy` locally).
|
||||
|
||||
## Adding new Github Actions
|
||||
|
||||
When adding new Github Actions the `.github\renovate.json` needs to be checked and add the new action to:
|
||||
|
||||
Generated
-3241
File diff suppressed because it is too large
Load Diff
@@ -5,10 +5,7 @@
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "npm run readme && npm run contributing && npm run license",
|
||||
"readme": "cross-env NODE_ENV=production node compile.js readme.mdx ../../readme.md",
|
||||
"license": "cross-env NODE_ENV=production node compile.js license.mdx ../../LICENSE.md",
|
||||
"contributing": "cross-env NODE_ENV=production node compile.js contributing.mdx ../contributing.md",
|
||||
"build": "cross-env NODE_ENV=production node compile.js readme.mdx ../../readme.md windows.mdx ../windows.md contributing.mdx ../contributing.md license.mdx ../../LICENSE.md",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
@@ -27,7 +24,7 @@
|
||||
"onFail": "error"
|
||||
},
|
||||
"packageManager": {
|
||||
"name": "npm",
|
||||
"name": "pnpm",
|
||||
"onFail": "error"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+2024
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
@@ -4,11 +4,15 @@
|
||||
|
||||
& $Env:ProgramFiles\Docker\Docker\DockerCli.exe -SwitchDaemon
|
||||
|
||||
## Toolchain versions
|
||||
|
||||
Windows toolchain versions (Git for Windows, Visual Studio BuildTools components, Windows 11 SDK build) are pinned in `config/version.json` under the `windows` block and validated by `config/schema.cue`. The `update_version.yml` workflow refreshes these alongside Flutter and Android in the monthly upgrade PR, and the Pester suite asserts the installed image matches the manifest on every CI run.
|
||||
|
||||
## TODO
|
||||
|
||||
1. Install tools
|
||||
|
||||
```powershell`
|
||||
```powershell
|
||||
# # needed? No
|
||||
# --add Microsoft.Component.MSBuild' `
|
||||
# # needed? No
|
||||
@@ -29,11 +33,10 @@ RUN Invoke-WebRequest -Uri https://aka.ms/vs/17/release/vs_buildtools.exe -OutFi
|
||||
Remove-Item vs_BuildTools.exe;
|
||||
```
|
||||
|
||||
1. Read dependencies from [flutter_tools](https://github.com/flutter/flutter/blob/master/packages/flutter_tools/lib/src/windows/visual_studio.dart).
|
||||
1. Check how it can be run in Github actions.
|
||||
1. Check how it can be run in Gitlab CI/CD.
|
||||
1. Test where is installed.
|
||||
1. Test that path to powershell.exe exists.
|
||||
1. Test with a snapshot of flutter config to determine if new feature flags should be enabled or disabled.
|
||||
1. Test that Build Tools were installed in C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\msbuild\current\bin
|
||||
1. Check [Windows installation requirements for Flutter](https://docs.flutter.dev/get-started/install/windows/desktop)
|
||||
1. Add docs explaining to use `$VerbosePreference = 'Continue';` in the SHELL to debug unexpected pwsh problems.
|
||||
@@ -73,13 +76,12 @@ Debug with `curl -A github165 -v https://mcr.microsoft.com/v2/powershell/manifes
|
||||
|
||||
1. Enable Windows Developer Settings to solve error:
|
||||
|
||||
>Building with plugins requires symlink support.
|
||||
>
|
||||
>Please enable Developer Mode in your system settings. Run
|
||||
> start ms-settings:developers
|
||||
>to open settings.
|
||||
|
||||
```powershell
|
||||
# >Building with plugins requires symlink support.
|
||||
# >
|
||||
# >Please enable Developer Mode in your system settings. Run
|
||||
# > start ms-settings:developers
|
||||
# >to open settings.
|
||||
reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1"
|
||||
```
|
||||
|
||||
@@ -87,7 +89,7 @@ Debug with `curl -A github165 -v https://mcr.microsoft.com/v2/powershell/manifes
|
||||
|
||||
1. Docker version must be pinned in Github workflow to avoid breaking changes: with escaping `\"` syntax inside RUN directive, etc.
|
||||
|
||||
1. Packaging tool in Windows: <https://pub.dev/packages/msix> . It uses the executables:
|
||||
1. Packaging tool in Windows: [msix](https://pub.dev/packages/msix) . It uses the executables:
|
||||
|
||||
- [makeappx.exe](https://learn.microsoft.com/en-us/windows/win32/appxpkg/make-appx-package--makeappx-exe-)
|
||||
- [makepri.exe](https://learn.microsoft.com/en-us/windows/uwp/app-resources/makepri-exe-command-options)
|
||||
@@ -108,7 +110,7 @@ Debug with `curl -A github165 -v https://mcr.microsoft.com/v2/powershell/manifes
|
||||
|
||||
- According to the [msstore guide](https://learn.microsoft.com/en-us/windows/apps/publish/msstore-dev-cli/commands?pivots=msstoredevcli-installer-linux#installation), It will be needed to install Microsoft.NetCore.Component.Runtime.8.0 with vs_BuildTools
|
||||
|
||||
1. From <https://github.com/tauu/flutter-windows-builder/blob/main/Dockerfile> => install <https://github.com/microsoft/StoreBroker> This is currently the primary tool to publish to Microsoft Store
|
||||
1. From [github.com/tauu/flutter-windows-builder/Dockerfile](https://github.com/tauu/flutter-windows-builder/blob/main/Dockerfile) => install [github.com/microsoft/StoreBroker](https://github.com/microsoft/StoreBroker) This is currently the primary tool to publish to Microsoft Store
|
||||
|
||||
- Not installed right now
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
<!--- This markdown file was auto-generated from "windows.mdx" -->
|
||||
|
||||
# Windows
|
||||
|
||||
## Swich between Linux and Windows containers
|
||||
|
||||
& $Env:ProgramFiles\\Docker\\Docker\\DockerCli.exe -SwitchDaemon
|
||||
|
||||
## Toolchain versions
|
||||
|
||||
Windows toolchain versions (Git for Windows, Visual Studio BuildTools components, Windows 11 SDK build) are pinned in `config/version.json` under the `windows` block and validated by `config/schema.cue`. The `update_version.yml` workflow refreshes these alongside Flutter and Android in the monthly upgrade PR, and the Pester suite asserts the installed image matches the manifest on every CI run.
|
||||
|
||||
## TODO
|
||||
|
||||
1. Install tools
|
||||
|
||||
```powershell
|
||||
# # needed? No
|
||||
# --add Microsoft.Component.MSBuild' `
|
||||
# # needed? No
|
||||
# --add Microsoft.VisualStudio.Component.TestTools.BuildTools `
|
||||
# # needed? No
|
||||
# --add Microsoft.VisualStudio.Component.VC.ASAN `
|
||||
# # needed? no
|
||||
# # --add Microsoft.VisualStudio.Component.VC.Tools.x86.x64 `
|
||||
RUN Invoke-WebRequest -Uri https://aka.ms/vs/17/release/vs_buildtools.exe -OutFile vs_BuildTools.exe; `
|
||||
Start-Process vs_BuildTools.exe -ArgumentList '--quiet --wait --norestart --nocache `
|
||||
# # needed? yes
|
||||
# --add Microsoft.VisualStudio.Component.VC.CMake.Project `
|
||||
# # needed? Yes
|
||||
# --add Microsoft.VisualStudio.Component.Windows11SDK.22621 `
|
||||
# # needed?
|
||||
# --add Microsoft.VisualStudio.Workload.VCTools' `
|
||||
-Wait; `
|
||||
Remove-Item vs_BuildTools.exe;
|
||||
|
||||
```
|
||||
|
||||
1. Read dependencies from [flutter\_tools](https://github.com/flutter/flutter/blob/master/packages/flutter%5Ftools/lib/src/windows/visual%5Fstudio.dart).
|
||||
2. Check how it can be run in Github actions.
|
||||
3. Check how it can be run in Gitlab CI/CD.
|
||||
4. Test that path to powershell.exe exists.
|
||||
5. Test that Build Tools were installed in C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools\\msbuild\\current\\bin
|
||||
6. Check [Windows installation requirements for Flutter](https://docs.flutter.dev/get-started/install/windows/desktop)
|
||||
7. Add docs explaining to use `$VerbosePreference = 'Continue';` in the SHELL to debug unexpected pwsh problems.
|
||||
|
||||
## Open issue in windows Docker images repo
|
||||
|
||||
1. Some images can be pulled while others give error:
|
||||
```text
|
||||
Error response from daemon: Get "https://mcr.microsoft.com/v2/": read tcp [2a0c:5a84:e100:e501::a97c]:58039->[2603:1061:f:101::10]:443: wsarecv: An existing connection was forcibly closed by the remote host.
|
||||
```
|
||||
|
||||
Debug with `curl -A github165 -v https://mcr.microsoft.com/v2/powershell/manifests/lts-nanoserver-ltsc2022`
|
||||
|
||||
## Contribute flutter upstream
|
||||
|
||||
1. Remove `WHERE` in bin\\internal\\shared.bat and use instead:
|
||||
```batch
|
||||
pwsh.exe -Command "exit" >nul 2>&1 && (
|
||||
SET powershell_executable=pwsh.exe
|
||||
) || powershell.exe -Command "exit" >nul 2>&1 && (
|
||||
SET powershell_executable=PowerShell.exe
|
||||
) || (
|
||||
ECHO Error: PowerShell executable not found. 1>&2
|
||||
ECHO Either pwsh.exe or PowerShell.exe must be in your PATH. 1>&2
|
||||
EXIT 1
|
||||
)
|
||||
```
|
||||
2. Find if the executable should be pwsh or powershell and put it in a service to remove the hardcoded "powershell" in multiple places, like in:
|
||||
* dev\\devicelab\\lib\\framework\\running\_processes.dart
|
||||
* packages\\flutter\_tools\\lib\\src\\windows\\windows\_version\_validator.dart
|
||||
|
||||
## Steps to reproduce in Docker
|
||||
|
||||
1. Enable Windows Developer Settings to solve error:
|
||||
```powershell
|
||||
# >Building with plugins requires symlink support.
|
||||
# >
|
||||
# >Please enable Developer Mode in your system settings. Run
|
||||
# > start ms-settings:developers
|
||||
# >to open settings.
|
||||
reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1"
|
||||
```
|
||||
2. For CI/CD
|
||||
1. Docker version must be pinned in Github workflow to avoid breaking changes: with escaping `\"` syntax inside RUN directive, etc.
|
||||
2. Packaging tool in Windows: [msix](https://pub.dev/packages/msix) . It uses the executables:
|
||||
* [makeappx.exe](https://learn.microsoft.com/en-us/windows/win32/appxpkg/make-appx-package--makeappx-exe-)
|
||||
* [makepri.exe](https://learn.microsoft.com/en-us/windows/uwp/app-resources/makepri-exe-command-options)
|
||||
* [signtool.exe](https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe)
|
||||
* certificate
|
||||
* Make a note that --install-certificate should be "false" or configured because the certificate can't be installed as ContainerUser.
|
||||
```powershell
|
||||
# OK
|
||||
Import-PfxCertificate -FilePath "C:\Users\ContainerUser\AppData\Local\Pub\Cache\hosted\pub.dev\msix-3.16.8\lib\assets\test_certificate.pfx" -Password (ConvertTo-SecureString -AsPlainText -Force "1234") -CertStoreLocation Cert:\LocalMachine\Root
|
||||
# Doesn't work
|
||||
Import-PfxCertificate -FilePath "C:\Users\ContainerUser\AppData\Local\Pub\Cache\hosted\pub.dev\msix-3.16.8\lib\assets\test_certificate.pfx" -Password (ConvertTo-SecureString -AsPlainText -Force "1234")
|
||||
```
|
||||
3. Install msstore CLI <https://github.com/microsoft/msstore-cli> It seems behind StoreBroker but it looks that it's going to be the primary and recommended way to publish to Microsoft Store
|
||||
* According to the [msstore guide](https://learn.microsoft.com/en-us/windows/apps/publish/msstore-dev-cli/commands?pivots=msstoredevcli-installer-linux#installation), It will be needed to install Microsoft.NetCore.Component.Runtime.8.0 with vs\_BuildTools
|
||||
4. From [github.com/tauu/flutter-windows-builder/Dockerfile](https://github.com/tauu/flutter-windows-builder/blob/main/Dockerfile) \=> install [github.com/microsoft/StoreBroker](https://github.com/microsoft/StoreBroker) This is currently the primary tool to publish to Microsoft Store
|
||||
* Not installed right now
|
||||
5. Install the [Windows App Certification Kit](https://learn.microsoft.com/en-us/windows/uwp/debug-test-perf/windows-app-certification-kit) or the [Windows SDK that already includes it](https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/)
|
||||
* Installed currently by one of the workloads in vs\_BuildTools
|
||||
|
||||
## References
|
||||
|
||||
* [How environment variables work on Windows containers?](https://blog.sixeyed.com/windows-weekly-dockerfile-14-environment-variables/)
|
||||
* [Windows deployment in Flutter](https://docs.flutter.dev/deployment/windows)
|
||||
* [vs\_BuildTools workloads](https://learn.microsoft.com/en-us/visualstudio/install/workload-component-id-vs-build-tools?view=vs-2022&preserve-view=true)
|
||||
* Useful Dockerfile <https://git.openprivacy.ca/openprivacy/flutter-desktop/src/branch/main/windows/Dockerfile>
|
||||
@@ -0,0 +1,6 @@
|
||||
[tools]
|
||||
cue = "0.15.0"
|
||||
git-cliff = "2.10.1"
|
||||
pnpm = "11.2.2"
|
||||
node = "lts"
|
||||
"github:gmeligio/gx" = "0.7.1"
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-05-01
|
||||
@@ -0,0 +1,100 @@
|
||||
## Context
|
||||
|
||||
Today, every `uses:` reference in `.github/workflows/*.yml` is pinned to a 40-character commit SHA with a trailing `# vX.Y.Z` comment. Pinning is enforced informally: a reviewer notices an unpinned PR. There is no manifest of intended versions, no lock with auditable timestamps, and no automated check.
|
||||
|
||||
Renovate (`.github/renovate.json`) opens monthly grouped PRs against the `github-tags` datasource and a `customManagers` regex covers Debian deb packages in the Dockerfile. ~16 distinct actions are in use across 8 workflows.
|
||||
|
||||
`gmeligio/gx` is a Rust CLI by the same author as this repo. Its **SHA-first resolution** strategy means it can read the existing pins and reconstruct a manifest + lock without modifying any workflow line. It is distributed via Homebrew, Cargo, and pre-built GitHub releases.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- A single declarative source of truth for the GitHub Actions versions this repo depends on.
|
||||
- Reproducible installs: any contributor or CI run can resolve the same SHA for the same constraint.
|
||||
- Fail PRs that introduce unpinned actions.
|
||||
- Keep Renovate's PR automation — gx complements rather than replaces it.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Replacing the existing `config/version.json` machinery for Flutter / Android / Fastlane SDKs (different domain, different update cadence).
|
||||
- Replacing Renovate's Dockerfile `customManagers` regex for Debian deb packages.
|
||||
- Authoring an upgrade-PR bot from scratch (`gx upgrade` is run manually or via a follow-up cron change).
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1: Adopt gx in file-backed mode (manifest + lock)
|
||||
|
||||
`gx` supports a memory-only mode (one-off `gx tidy`), but we want team-level reproducibility and a CI lint gate, both of which require the manifest + lock. Generate them with `gx init`.
|
||||
|
||||
**Alternative considered**: stay memory-only. Rejected — without a lock, `gx lint` cannot detect drift, and version constraints can't be expressed (`^6` vs the literal SHA in workflows).
|
||||
|
||||
### Decision 2: Keep Renovate, add gx as authority
|
||||
|
||||
Renovate continues to open the monthly upgrade PR. After bumping a SHA, it must run `gx tidy` so `.github/gx.lock` matches the updated workflow. Implementation options:
|
||||
|
||||
- (a) `postUpgradeTasks` in `renovate.json` (recommended — one place, runs as part of the same PR).
|
||||
- (b) A separate workflow listening on PR open that runs `gx tidy` and pushes a fixup commit.
|
||||
|
||||
Pick (a). It is supported by Renovate self-hosted and by the GitHub-hosted app for repositories that allow it; if the GitHub-hosted Renovate app blocks `postUpgradeTasks`, fall back to (b).
|
||||
|
||||
**Alternative considered**: Replace Renovate's `github-tags` datasource with a `gx upgrade` cron workflow that opens PRs via `peter-evans/create-pull-request` (already used in `update_version.yml`). Rejected for the initial migration — Renovate's release-notes integration and grouping are valuable; we can revisit if the postUpgradeTasks hook proves brittle.
|
||||
|
||||
### Decision 3: Install gx in CI via the existing `jaxxstorm/action-install-gh-release`
|
||||
|
||||
That action is already used in `build.yml`, `ci.yml`, `changelog.yml`, `update_version.yml`. Reuse for gx — no new tooling pattern.
|
||||
|
||||
**Alternative considered**: `cargo install gx`. Rejected — adds a Rust toolchain dependency to CI for a single binary.
|
||||
|
||||
### Decision 4: Manifest version constraints default to caret on major (e.g., `^6`)
|
||||
|
||||
Mirrors the manifest gx itself ships in its own repo. Lets patches and minor versions auto-resolve while requiring a deliberate change for majors. Where the current pin is on a v0 action (e.g., `peter-evans/dockerhub-description@v4` is v4 but some actions are v0/v1) or a known-unstable action, pin tighter (`~1.18.2`).
|
||||
|
||||
### Decision 5: `gx lint` is a required CI check
|
||||
|
||||
Add a job to `.github/workflows/ci.yml` that runs on every PR. It must:
|
||||
|
||||
- Fail if any `uses:` is not SHA-pinned.
|
||||
- Fail if `.github/gx.lock` does not match the SHAs in the workflows.
|
||||
|
||||
This is the gate that makes the manifest meaningful.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **Lock-file drift if Renovate edits a SHA without running `gx tidy`** → `gx lint` fails the PR; reviewer or Renovate's `postUpgradeTasks` re-runs `gx tidy`. Worst case: a one-line manual fix.
|
||||
- **gx is a young tool (one author, this user)** → Adoption risk is low because the user owns both repos, but document a rollback path: deleting `.github/gx.toml` and `.github/gx.lock` and removing the lint job restores the prior state — workflows are unchanged.
|
||||
- **`postUpgradeTasks` may be disabled on the GitHub-hosted Renovate app** → Fall back to a small `gx-tidy.yml` workflow triggered on `pull_request` that runs `gx tidy` and pushes a fixup commit when the lock drifts.
|
||||
- **Network dependency at lint time** → `gx lint` against the lock should be offline; only `gx upgrade` hits the GitHub API. Verify during implementation; if `gx lint` requires API calls, configure `GITHUB_TOKEN` for the CI step.
|
||||
- **Two tools touching the same files** → Mitigated by Decision 2: gx runs *after* Renovate inside the same PR, never in parallel.
|
||||
|
||||
## Automated Test Strategy
|
||||
|
||||
- **Manifest generation correctness**: After `gx init`, run `gx lint` locally — expect zero diff. Commit only if clean.
|
||||
- **CI integration test**: Open a draft PR that intentionally bumps an action's SHA without updating the lock. Confirm `gx lint` fails. Then run `gx tidy`, push, and confirm green.
|
||||
- **Renovate integration test**: Manually trigger a Renovate run against a test branch (or wait for the next scheduled run after merge); verify the PR includes both workflow SHA changes and `.github/gx.lock` updates.
|
||||
- **Critical path**: the lint job in CI. If it can't reliably distinguish "pinned and locked" from "unpinned or drifted," the manifest provides no value.
|
||||
- **No new test infrastructure** is needed — existing CI runners and the standard GitHub Actions PR flow cover everything.
|
||||
|
||||
## Observability
|
||||
|
||||
- `gx lint` writes its diagnostics to stdout/stderr; CI annotations surface them on the PR.
|
||||
- Failure modes that could be silent:
|
||||
- **Renovate `postUpgradeTasks` not running** (e.g., disabled on hosted Renovate). Mitigation: `gx lint` will catch it on the PR — failure is loud, not silent.
|
||||
- **`gx lint` skipped due to a workflow filter mistake**. Mitigation: in code review of `ci.yml`, verify the gx-lint job has no `if:` exclusions and runs on all PRs touching `.github/**`.
|
||||
- Log retention is GitHub's default (90 days for workflow logs) — sufficient for after-the-fact triage.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Install `gx` locally; run `gx init` to generate `.github/gx.toml` and `.github/gx.lock`.
|
||||
2. Run `gx tidy` — expect a no-op diff. Commit both files.
|
||||
3. Add the `gx lint` job to `.github/workflows/ci.yml`. Open a PR; confirm green.
|
||||
4. Add `postUpgradeTasks` (or fallback workflow) to keep Renovate PRs in sync.
|
||||
5. Update `docs/contributing.md` with the local workflow.
|
||||
6. Monitor the next Renovate-driven PR; verify `gx tidy` runs and the lock updates correctly.
|
||||
|
||||
**Rollback**: revert the change. Workflow files were never modified, so deletion of `.github/gx.toml`, `.github/gx.lock`, and the lint job restores prior state.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Does the GitHub-hosted Renovate app permit `postUpgradeTasks`? Verify against current Mend/Renovate documentation before committing to Decision 2(a). If blocked, ship 2(b) directly.
|
||||
- Should the `gx lint` job run on the `main` push as well as PRs? Defer — PR-only is sufficient for the gate; main-push lint can be added later if drift somehow lands.
|
||||
@@ -0,0 +1,30 @@
|
||||
## Why
|
||||
|
||||
GitHub Actions in this repo are SHA-pinned today, but version policy lives only as `# v6.0.1` comments — there is no declarative manifest, no reproducible lock, and no CI gate that fails a PR which introduces an unpinned action. Adopting `gmeligio/gx` gives us a TOML manifest, a lock file, and a `gx lint` step that enforces pinning, while leaving Renovate to keep opening upgrade PRs.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add `gmeligio/gx` as the source of truth for GitHub Actions versions via `.github/gx.toml` (semantic constraints) and `.github/gx.lock` (resolved SHAs).
|
||||
- Generate both files from the current SHA-pinned workflows using `gx init` (SHA-first resolution — no workflow rewrites required).
|
||||
- Add a `gx lint` job to CI that fails the build when any `uses:` reference is unpinned or drifts from the lock.
|
||||
- Teach Renovate-driven PRs to refresh the gx lock: add a post-update hook (or repo workflow) that runs `gx tidy` and amends the PR so workflows and lock stay in sync.
|
||||
- Document the local workflow in `docs/contributing.md`: contributors who edit workflow files must run `gx tidy` before committing.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `actions-version-tracking`: Declarative manifest + lock for GitHub Actions, plus a CI lint that enforces SHA pinning and lock-file consistency on every PR.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
<!-- None — no prior specs exist in openspec/specs/. -->
|
||||
|
||||
## Impact
|
||||
|
||||
- **Files added**: `.github/gx.toml`, `.github/gx.lock`.
|
||||
- **Files modified**: `.github/workflows/ci.yml` (add `gx lint` job), `.github/renovate.json` (add `postUpgradeTasks` to run `gx tidy` on github-actions PRs), `docs/contributing.md` (workflow editing instructions).
|
||||
- **Workflows touched**: read-only — `gx init` reads existing pins; lock + manifest are derived. No `uses:` line is rewritten.
|
||||
- **Tooling**: contributors and CI need `gx` available. Install via `cargo install gx`, `brew install gmeligio/tap/gx`, or `jaxxstorm/action-install-gh-release` (already used elsewhere in this repo).
|
||||
- **Renovate**: continues to open upgrade PRs for `github-tags` datasource. The `customManagers` regex for Debian deb packages in the Dockerfile is unaffected — gx does not handle that.
|
||||
- **Risk**: lock-file drift if a Renovate PR merges without running `gx tidy`. Mitigated by `gx lint` failing CI when the lock and workflows disagree.
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Manifest as source of truth for GitHub Actions versions
|
||||
|
||||
The repository SHALL maintain a `.github/gx.toml` manifest declaring the version constraint for every distinct GitHub Action referenced by `uses:` in any workflow file under `.github/workflows/` and any composite action under `.github/actions/`.
|
||||
|
||||
#### Scenario: Manifest covers every action in workflows
|
||||
|
||||
- **WHEN** a workflow file references `uses: <owner>/<repo>@<ref>`
|
||||
- **THEN** `.github/gx.toml` contains an entry under `[actions]` for `"<owner>/<repo>"` with a SemVer-style constraint
|
||||
- **AND** the constraint is a caret range on the current major (e.g., `^6`) unless a tighter pin is explicitly justified
|
||||
|
||||
#### Scenario: Adding a new action requires manifest update
|
||||
|
||||
- **WHEN** a contributor opens a PR introducing a new `uses: <owner>/<repo>@<ref>` reference
|
||||
- **THEN** `gx tidy` adds the action to `.github/gx.toml` and `.github/gx.lock`
|
||||
- **AND** the PR fails CI if either file is missing the new entry
|
||||
|
||||
### Requirement: Lock file records resolved SHAs
|
||||
|
||||
The repository SHALL maintain a `.github/gx.lock` file that records, for every manifest entry, the resolved version, the immutable commit SHA, the source repository, the ref type, and the resolution date.
|
||||
|
||||
#### Scenario: Lock file matches workflow SHAs
|
||||
|
||||
- **WHEN** `gx lint` runs
|
||||
- **THEN** every `uses: <owner>/<repo>@<sha>` in workflows resolves to an entry under `[actions."<owner>/<repo>"]` in `.github/gx.lock` whose `sha` field equals the SHA in the workflow
|
||||
|
||||
#### Scenario: Lock file is regenerated by gx tidy
|
||||
|
||||
- **WHEN** a contributor runs `gx tidy` after editing a workflow or the manifest
|
||||
- **THEN** `.github/gx.lock` is updated to reflect the new resolutions
|
||||
- **AND** all `uses:` references in workflows are rewritten to the SHA + `# vX.Y.Z` comment format
|
||||
|
||||
### Requirement: Workflow references are SHA-pinned with version comments
|
||||
|
||||
Every `uses:` reference to a third-party GitHub Action in this repository SHALL be pinned to a 40-character commit SHA followed by a trailing comment of the form `# vX.Y.Z` (or `# vX` / `# vX.Y` for actions that publish such tags).
|
||||
|
||||
#### Scenario: Unpinned tag reference is rejected in CI
|
||||
|
||||
- **WHEN** a PR contains a `uses:` reference like `actions/checkout@v6` (a tag rather than a SHA)
|
||||
- **THEN** the `gx lint` CI job fails
|
||||
- **AND** the failure message identifies the unpinned reference
|
||||
|
||||
#### Scenario: Local actions are not subject to pinning
|
||||
|
||||
- **WHEN** a workflow uses a local action (e.g., `uses: ./.github/actions/clean-runner-disk`)
|
||||
- **THEN** `gx lint` ignores it
|
||||
- **AND** no manifest or lock entry is required
|
||||
|
||||
### Requirement: CI enforces manifest, lock, and pin consistency
|
||||
|
||||
The repository's CI SHALL run `gx lint` on every pull request and SHALL fail when any of the following are true: a workflow `uses:` is unpinned, the manifest does not list a referenced action, or the lock file SHA does not match the workflow SHA.
|
||||
|
||||
#### Scenario: PR with drifted lock fails CI
|
||||
|
||||
- **WHEN** a PR updates a workflow SHA without regenerating `.github/gx.lock`
|
||||
- **THEN** `gx lint` fails the PR check
|
||||
- **AND** the failure output indicates which action's lock entry is stale
|
||||
|
||||
#### Scenario: Clean PR passes the gx-lint job
|
||||
|
||||
- **WHEN** a PR's workflow files, manifest, and lock are mutually consistent and every `uses:` is SHA-pinned
|
||||
- **THEN** the `gx lint` CI job exits 0
|
||||
- **AND** the check appears as passing on the PR
|
||||
|
||||
### Requirement: Renovate-driven upgrades keep the lock in sync
|
||||
|
||||
Renovate-generated upgrade pull requests for GitHub Actions SHALL leave `.github/gx.lock` consistent with the workflow SHAs they introduce, either by running `gx tidy` as a `postUpgradeTask` in `renovate.json` or via an equivalent CI workflow that pushes a fixup commit to the PR before merge.
|
||||
|
||||
#### Scenario: Renovate bumps a SHA and the lock is updated
|
||||
|
||||
- **WHEN** Renovate opens a PR that bumps `actions/checkout` from one SHA to another
|
||||
- **THEN** the same PR contains a corresponding update to `.github/gx.lock`
|
||||
- **AND** `gx lint` passes on the PR
|
||||
|
||||
#### Scenario: Lock drift is detected before merge
|
||||
|
||||
- **WHEN** for any reason a Renovate PR lands without the lock update
|
||||
- **THEN** the `gx lint` CI job fails the PR
|
||||
- **AND** merge is blocked until `gx tidy` is run and committed
|
||||
|
||||
### Requirement: Contributor documentation describes the gx workflow
|
||||
|
||||
`docs/contributing.md` SHALL document the local workflow for editing `.github/workflows/*.yml` files: install `gx`, run `gx tidy` after edits, and commit the resulting changes to `.github/gx.toml` and `.github/gx.lock` along with the workflow change.
|
||||
|
||||
#### Scenario: Contributor reads the workflow editing guide
|
||||
|
||||
- **WHEN** a contributor opens `docs/contributing.md` looking for guidance on editing workflow files
|
||||
- **THEN** they find instructions to install `gx` and run `gx tidy` before committing
|
||||
- **AND** they find a reference link to `https://github.com/gmeligio/gx`
|
||||
@@ -0,0 +1,36 @@
|
||||
## 1. Generate manifest and lock locally
|
||||
|
||||
- [x] 1.1 Install `gx` locally (`brew install gmeligio/tap/gx` or `cargo install gx`) and verify with `gx --version`
|
||||
- [x] 1.2 Run `gx init` at the repo root; confirm it generates `.github/gx.toml` and `.github/gx.lock` from existing SHA-pinned workflows
|
||||
- [x] 1.3 Inspect `.github/gx.toml` constraints; tighten any v0/v1 actions to `~X.Y.Z`, leave stable majors as `^X`
|
||||
- [x] 1.4 Run `gx tidy`; confirm zero diff against workflow files
|
||||
- [x] 1.5 Run `gx lint`; confirm zero findings
|
||||
|
||||
## 2. Wire up CI lint gate
|
||||
|
||||
- [x] 2.1 Add a `gx_lint` job to `.github/workflows/build.yml` (deviation: ci.yml only triggers on push; build.yml is the PR-time workflow). Runs on `pull_request` via existing trigger.
|
||||
- [x] 2.2 Install `gx` in the job using `jaxxstorm/action-install-gh-release@6096f2a2bbfee498ced520b6922ac2c06e990ed2 # v2.1.0`, pointing at `gmeligio/gx` tag `v0.7.1` with archive digest `6632843410c877c43aa8936eb757d8b0ddcb5940402203914543ef8a9cf8ecd9`.
|
||||
- [x] 2.3 Run `gx lint` as the job step with `GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}` to avoid the 60 req/h anonymous rate limit.
|
||||
- [x] 2.4 Open a draft PR that intentionally edits a workflow SHA without rerunning `gx tidy`; confirm `gx-lint` fails
|
||||
- [x] 2.5 Run `gx tidy` on the same PR; confirm `gx-lint` passes
|
||||
|
||||
## 3. Keep Renovate-driven PRs in sync
|
||||
|
||||
- [x] 3.1 Decided: skip the `postUpgradeTasks` route (3.2/3.3) — the GitHub-hosted Mend Renovate app does not permit `postUpgradeTasks` without org-level allowlisting, so the fallback workflow is the more reliable mechanism and works for any PR (Renovate or human) that drifts the lock.
|
||||
- [~] 3.2 Skipped — see 3.1.
|
||||
- [~] 3.3 Skipped — see 3.1.
|
||||
- [x] 3.4 Added `.github/workflows/gx-tidy.yml` triggered on `pull_request` (paths `.github/workflows/**`, `.github/gx.toml`, `.github/gx.lock`). Uses the `VERIFIED_COMMIT_ID`/`VERIFIED_COMMIT_KEY` GitHub App (same one used by `tag.yml`/`changelog.yml`/`release.yml`/`update_version.yml`) to push the fixup commit. Skips PRs from forks via `if: head.repo.full_name == github.repository`.
|
||||
- [x] 3.5 Open a test Renovate-style PR that bumps a single action SHA; confirm the lock is updated within the same PR before merge.
|
||||
|
||||
## 4. Documentation
|
||||
|
||||
- [x] 4.1 Add a "Editing GitHub Actions workflows" section to `docs/contributing.md` explaining the `gx tidy` local step and linking to `https://github.com/gmeligio/gx`
|
||||
- [x] 4.2 Mention the `gx-lint` CI gate so contributors know what will fail their PR
|
||||
- [x] 4.3 Reference the manifest format and a one-line snippet showing how to add a new action
|
||||
|
||||
## 5. Verification and rollout
|
||||
|
||||
- [x] 5.1 Run `openspec validate adopt-gx-for-actions` and resolve any findings
|
||||
- [x] 5.2 Open the implementation PR; confirm all CI jobs pass including the new `gx-lint`
|
||||
- [x] 5.3 After merge, monitor the next Renovate-driven github-actions PR; confirm `gx.lock` is updated alongside workflow SHAs
|
||||
- [x] 5.4 Document rollback steps inline in the PR description (delete `.github/gx.toml`, `.github/gx.lock`, the lint job, and any `postUpgradeTasks` block — workflows themselves remain untouched)
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-05-09
|
||||
@@ -0,0 +1 @@
|
||||
passed
|
||||
@@ -0,0 +1,118 @@
|
||||
## Context
|
||||
|
||||
`update_version.yml` is a four-job pipeline:
|
||||
|
||||
```
|
||||
update_flutter_version → update_android_version → validate_config_version → update_docs_and_create_pr
|
||||
(host) (container) (host) (host)
|
||||
```
|
||||
|
||||
Job 1 decides "is there a new Flutter?" and emits an artifact + a `result` step output. Every later job is gated by `if: needs.update_flutter_version.outputs.new_version == 'true'` directly or transitively.
|
||||
|
||||
Today on `single_update`, job 1 runs two overlapping steps:
|
||||
|
||||
1. A **new** CUE step (`Fetch and update latest Flutter version`) that fetches `releases_linux.json`, runs `cue import` + `cue eval --force --outfile config/flutter_version.json`. It does NOT compare or set any output.
|
||||
2. The **old** github-script step (`Update latest Flutter version`) that reads `config/flutter_version.json` from disk, fetches the same JSON, compares against the on-disk version, returns `true`/`false`.
|
||||
|
||||
Because the new step writes the file before the old step reads it, the old step always sees no diff and returns `false`. The pipeline becomes a permanent no-op.
|
||||
|
||||
Constraints:
|
||||
|
||||
- The workflow must keep using `actions-version-tracking` pins (gx) — every `uses:` is SHA-pinned and version-tracked in `.github/gx.toml`. New `uses:` lines (none planned) would need a manifest entry.
|
||||
- `cue@0.15.0` is already installed in the workflow via `jaxxstorm/action-install-gh-release` — reuse that.
|
||||
- The existing `script/copyFlutterVersion.js` runs in job 2 inside the `flutter-android` container and merges the `flutter` sub-document into the larger `version.json`. It's orthogonal to job 1 and stays.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Job 1 reaches "new version available" → `result == 'true'` → downstream jobs run, exactly when the upstream stable version changes.
|
||||
- The Android `buildTools` version in `version.json` is sourced from Flutter's `packages.txt` for the new tag, replacing the current orphan curl step with a wired-in value consumed by the CUE `version.json` generator.
|
||||
- `cue vet` passes against the committed `config/flutter_version.json` and against any `version.json` produced by the workflow.
|
||||
- `script/updateFlutterVersion.js` is deleted (the goal of the branch — single CUE-driven flow).
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Rewriting `script/copyFlutterVersion.js` into CUE. It's tangential and works.
|
||||
- Replacing the github-script step in job 2 that updates the Fastlane version (`updateFastlaneVersion.js`) — separate concern.
|
||||
- Restructuring the four-job pipeline. Same job names, same `needs:` graph, same artifact handoffs.
|
||||
- Changing the cron schedule.
|
||||
- Anything related to the docs build in job 4.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1: Replace JS with one shell+CUE step, not a multi-step CUE pipeline
|
||||
|
||||
The new job-1 step does the full read-old / fetch / compare / conditionally-write / set-output sequence in one bash block, calling `cue` for the heavy lifting (parsing JSON, pulling fields, writing the new file). Rationale: keeping it in one step keeps the `result` output local and avoids an inter-step file-handoff dance. CUE is used where it adds value (typed JSON manipulation, schema validation) and bash is used where it's natural (HTTP, comparison, `$GITHUB_OUTPUT`). A pure-CUE pipeline would need a wrapper script anyway.
|
||||
|
||||
Alternative considered: split into 4 steps mirroring the inline TODO comments. Rejected — every split needs an artifact or `$GITHUB_OUTPUT` plumbing for the next step, with no real benefit.
|
||||
|
||||
### Decision 2: Source build-tools version with a CUE-aware shell step in job 2
|
||||
|
||||
The build-tools version comes from `https://raw.githubusercontent.com/flutter/flutter/refs/tags/<FLUTTER_VERSION>/engine/src/flutter/tools/android_sdk/packages.txt`. The current orphan step already does the curl+awk extraction. We give it an `id`, write to `$GITHUB_OUTPUT`, then plumb the value into the existing `script/update_test.sh` / CUE `version.json` generation path so it lands in `config/version.json`.
|
||||
|
||||
Open implementation question (Open Question 1): the current `update_test.sh` derives `android_sdk_build_tools_version` from gradle output via `updateAndroidVersions.gradle.kts`. We need to choose whether `packages.txt` *replaces* the gradle source or *cross-checks* it. Default: replace, since `packages.txt` is the upstream source of truth Flutter itself uses.
|
||||
|
||||
Alternative considered: query the gradle plugin only and trust it. Rejected — the user explicitly asked for `packages.txt` as the source.
|
||||
|
||||
### Decision 3: Channel restricted to `"stable"` in the schema
|
||||
|
||||
`config/schema.cue#FlutterVersion.flutter.channel` becomes literal `"stable"` (not a disjunction). Rationale: the fetcher already filters by `^\d+\.\d+\.\d+$`, which stable-only releases match. The schema change makes that invariant explicit and rejects accidental beta/dev pins at validation time.
|
||||
|
||||
Alternative considered: keep `"stable" | "beta"`. Rejected — user confirmed stable-only is intentional.
|
||||
|
||||
### Decision 4: Keep `script/copyFlutterVersion.js`, delete `script/updateFlutterVersion.js`
|
||||
|
||||
Only the **fetch + compare** is migrating to CUE. The merge of `flutter_version.json` into `version.json` (job 2 step) stays as-is. Rationale: scope. The branch's goal as named (`single_update`) is the fetch path; broadening scope to also rewrite the merge would multiply the change surface for no user-visible benefit.
|
||||
|
||||
### Decision 5: Output `result` as a step output, not a job output value derived from artifact presence
|
||||
|
||||
The old job-2 `if:` gate reads `needs.update_flutter_version.outputs.new_version`, which today maps to `steps.update_flutter_version.outputs.result`. Keep the same shape: the new step has `id: update_flutter_version` and writes `result=true|false` to `$GITHUB_OUTPUT`. Job-level outputs and downstream `if:` conditions remain literally unchanged.
|
||||
|
||||
Alternative considered: replace `result` with "did we upload the artifact" — rejected, would touch every downstream `if:` for no gain.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[Risk] `cue import` output shape may not expose `releases` as a top-level field.** `cue import file.json` produces a CUE file whose top-level fields mirror the JSON's top-level keys. `releases_linux.json`'s top level is `{"base_url": ..., "current_release": {...}, "releases": [...] }`, so `releases[0].channel` should resolve. Could not verify live (sandbox network timeouts to googleapis). → **Mitigation:** the implementation step runs `cue eval` with a smoke value and a fallback to `--list` mode; if the shape is different, implementer iterates locally with `mise use cue@0.15.0`.
|
||||
|
||||
- **[Risk] `packages.txt` format changes.** A future Flutter version could split `build-tools` into multiple lines or change the `;` delimiter. Today: `grep 'build-tools' | awk -F'[;:]' '{print $2}'`. → **Mitigation:** if the extraction returns empty or a non-semver string, the CUE schema (`buildTools!: #SemverPatch`) rejects it at the `validate_config_version` job, failing loudly rather than silently shipping a bad image.
|
||||
|
||||
- **[Risk] First post-merge run produces a large PR (potentially several Flutter versions worth of catch-up).** The repo has been unable to bump versions while this branch was broken. → **Mitigation:** none needed — that's the desired behavior; the maintainer reviews and merges as normal.
|
||||
|
||||
- **[Trade-off] CUE installed twice in the workflow (job 1 host, job 2 container) and shell logic in job 1.** The job-2 install is unavoidable (different runner). Bash-glue in job 1 is a small cost for keeping `result` plumbing colocated.
|
||||
|
||||
## Automated Test Strategy
|
||||
|
||||
Manual + CI:
|
||||
|
||||
- Local: `cue vet config/schema.cue -d '#FlutterVersion' config/flutter_version.json` (run via `mise use cue@0.15.0`). Today fails (`#PatchVersion`); after the fix it must pass.
|
||||
- Local: `cue vet config/schema.cue -d '#Version' config/version.json` against the committed `config/version.json`. Must pass after the channel narrowing.
|
||||
- Local dry-run of the new fetch step as a shell script against a checked-in fixture of `releases_linux.json` to assert (a) when the fixture's latest stable matches the on-disk version, the script writes `result=false` and does NOT modify `config/flutter_version.json`; (b) when they differ, it writes `result=true` and overwrites the file with a `cue vet`-passing payload.
|
||||
- CI: trigger `update_version.yml` via `workflow_dispatch` on the branch before merge. Two trigger paths to exercise: (i) immediately, expecting either `true`-path-to-PR or `false`-path-to-clean-skip depending on whether main is current; (ii) after artificially rewinding `config/flutter_version.json` to an older version on the branch, expecting `true` path through to PR creation.
|
||||
- Critical path under test: job-1 step output → job-2 gate → `validate_config_version` `cue vet` → job-4 PR title/commit message non-empty.
|
||||
- No new test infrastructure required.
|
||||
|
||||
## Observability
|
||||
|
||||
- The new fetch step `echo`s the old version, the new version, and the comparison verdict before writing `$GITHUB_OUTPUT`. Visible in the run log without enabling debug logging.
|
||||
- `cue vet` failures produce explicit constraint-violation messages (already true in `build.yml` and `validate_config_version`).
|
||||
- The `Create commit message variable` step bug (writes to wrong file) is silent today — empty PR title masks the symptom. After the fix, the step echoes the resolved value, and an empty value would surface as a PR-creation step error rather than a silently-wrong PR.
|
||||
- Failure modes that must NOT be silent:
|
||||
- Fetch failure → step exits non-zero (no `set +e`), job goes red.
|
||||
- Empty/non-semver build-tools extraction → CUE schema rejects in `validate_config_version`, downstream PR job is skipped, run is red.
|
||||
- Schema reference errors → caught at `cue vet` in either `build.yml` or `validate_config_version`.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
This is a workflow + schema change with no runtime/image surface, so "deploy" = merge to `main`.
|
||||
|
||||
1. Land the change on `single_update`, push to GitHub.
|
||||
2. Trigger `update_version.yml` via `workflow_dispatch` on the branch.
|
||||
3. If green and either (a) PR opened against the branch, or (b) clean skip after job 1 — merge to main.
|
||||
4. First scheduled run after merge produces a real upgrade PR (or skips cleanly if main is already current).
|
||||
|
||||
**Rollback:** revert the merge commit. Workflow returns to the pre-`single_update` JS-based fetch — known-working state.
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Build-tools sourcing:** does `packages.txt` *replace* or *cross-check* the gradle-plugin extraction in `updateAndroidVersions.gradle.kts`? Default is replace, but `update_test.sh` and the gradle script may need to be adapted in the same change. To confirm during implementation by reading `script/update_test.sh` and the gradle script.
|
||||
@@ -0,0 +1,32 @@
|
||||
## Why
|
||||
|
||||
The `single_update` branch is mid-migration: it renamed the CUE schema, removed `flutter_version.cue`, and started a CUE-native replacement of `script/updateFlutterVersion.js` — but the migration is incomplete and currently broken. As-is, every scheduled run of `update_version.yml` finishes with no PR: the new CUE step rewrites `config/flutter_version.json` *before* the JS comparator runs, so the JS step always reports "no change" and every downstream job is skipped behind `if: result == 'true'`. On top of that, `cue vet` fails outright because `config/schema.cue` references the renamed-away `#PatchVersion`. CI engineers consuming `ghcr.io/.../flutter-android` therefore stop receiving Flutter updates entirely until this branch is finished.
|
||||
|
||||
Relevance gate: this is a spec-worthy change because the CI engineer pulling the image *notices* the absence of new Flutter versions. The pipeline behavior is what they observe, not the wiring underneath.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Replace `script/updateFlutterVersion.js` with a CUE-native step in `update_version.yml` that fetches `releases_linux.json`, reads the current pinned version, compares, and only writes `config/flutter_version.json` when the upstream stable version actually changed. The step exposes `result` ∈ {`true`,`false`} as a step output so existing downstream `if:` gates keep working unchanged.
|
||||
- **BREAKING** (internal): delete `script/updateFlutterVersion.js`. Nothing outside the workflow depends on it.
|
||||
- Source the Android `build-tools` version from Flutter's pinned `engine/src/flutter/tools/android_sdk/packages.txt` for the new Flutter tag, and feed that value into the CUE-driven `version.json` generation. The current orphan `Update Android SDK build tools version` step gets an `id` and its output is consumed.
|
||||
- Lock the Flutter channel to `"stable"` only in `config/schema.cue` (drop `"beta"`). The fetcher already filters to stable releases via `^\d+\.\d+\.\d+$`, so this just makes the schema match reality.
|
||||
- Fix `config/schema.cue:20`: replace the dangling `#PatchVersion` reference with `#SemverPatch` so `cue vet` passes.
|
||||
- Fix `config/android.cue:39`: the length guard checks `input.fileContentTests` but the body indexes `input.commandTests`. Should be `len(input.commandTests) >= 3`.
|
||||
- Fix `update_version.yml` "Create commit message variable" step: write `commit_message` to `$GITHUB_OUTPUT` (the consumer reads `steps.create_commit_message.outputs.commit_message`); today it writes to `$GITHUB_ENV` and the resulting PR has an empty title and commit message.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `flutter-version-update`: the scheduled pipeline that detects a new upstream Flutter stable release and opens a PR bumping the image. Covers what the CI engineer sees as a result of the daily scheduled run — whether a PR appears, what it contains, and when no PR is opened.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
_None._ `actions-version-tracking` (the `gx` manifest) is a separate concern and is not touched.
|
||||
|
||||
## Impact
|
||||
|
||||
- **Workflows**: `.github/workflows/update_version.yml` (rewritten fetch step, fixed commit-message step, build-tools wiring). `.github/workflows/build.yml` already references `config/schema.cue` correctly on this branch.
|
||||
- **Schema**: `config/schema.cue` (channel narrowed, undefined reference fixed). `config/android.cue` (length-guard typo).
|
||||
- **Scripts**: `script/updateFlutterVersion.js` deleted. `script/copyFlutterVersion.js` retained — it merges the Flutter sub-document into `version.json` after the Android job runs and has no overlap with the fetch step.
|
||||
- **Operational**: no secrets, no permissions, no image-runtime change. The first scheduled run after merge produces a PR for whatever Flutter version is current upstream. The next run is a no-op until upstream ships another stable release.
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Scheduled run opens an upgrade PR when a new stable Flutter is released
|
||||
|
||||
The `update_version.yml` workflow SHALL open exactly one pull request titled `chore(release): upgrade flutter to <version>` whenever the latest entry in `https://storage.googleapis.com/flutter_infra_release/releases/releases_linux.json` matching the stable channel and a `\d+.\d+.\d+` version differs from the version currently pinned in `config/flutter_version.json`.
|
||||
|
||||
The experience context is the CI engineer who watches this repository for upgrade PRs to merge into their image fork.
|
||||
|
||||
#### Scenario: Upstream ships a new stable Flutter
|
||||
|
||||
- **GIVEN** `config/flutter_version.json` pins Flutter `X.Y.Z`
|
||||
- **AND** the latest stable release in `releases_linux.json` is `X.Y.Z+1`
|
||||
- **WHEN** the scheduled run of `update_version.yml` executes
|
||||
- **THEN** a branch `update-flutter-dependencies/X.Y.Z+1` is pushed
|
||||
- **AND** a pull request is opened with title `chore(release): upgrade flutter to X.Y.Z+1`
|
||||
- **AND** the commit message on that PR equals the title (non-empty)
|
||||
|
||||
#### Scenario: No upstream change since last run
|
||||
|
||||
- **GIVEN** `config/flutter_version.json` already pins the latest stable Flutter version
|
||||
- **WHEN** the scheduled run of `update_version.yml` executes
|
||||
- **THEN** no branch is created
|
||||
- **AND** no pull request is opened
|
||||
- **AND** all jobs after `update_flutter_version` are skipped
|
||||
|
||||
### Requirement: Upgrade PR contains a coherent, validated `version.json`
|
||||
|
||||
When the workflow opens an upgrade PR, the included `config/version.json` SHALL satisfy `cue vet config/schema.cue -d '#Version'` and SHALL contain the Android `buildTools.version` listed for that exact Flutter tag in `engine/src/flutter/tools/android_sdk/packages.txt` upstream.
|
||||
|
||||
The experience context is the CI engineer reviewing or merging the upgrade PR — they observe that downstream image builds will not silently regress on Android tooling.
|
||||
|
||||
#### Scenario: Build-tools version tracks the new Flutter tag
|
||||
|
||||
- **GIVEN** the workflow is opening an upgrade PR for Flutter `X.Y.Z`
|
||||
- **AND** Flutter's `engine/src/flutter/tools/android_sdk/packages.txt` at tag `X.Y.Z` lists `build-tools;A.B.C`
|
||||
- **WHEN** the PR is created
|
||||
- **THEN** `config/version.json` in the PR contains `android.buildTools.version == "A.B.C"`
|
||||
|
||||
#### Scenario: Generated config is schema-valid
|
||||
|
||||
- **GIVEN** the workflow has produced a candidate `config/version.json`
|
||||
- **WHEN** the `validate_config_version` job runs
|
||||
- **THEN** `cue vet config/schema.cue -d '#Version' config/version.json` exits 0
|
||||
- **AND** the workflow only proceeds to open the PR if validation passes
|
||||
|
||||
### Requirement: Schema rejects non-stable Flutter channels
|
||||
|
||||
`config/schema.cue` SHALL constrain `flutter.channel` to the literal `"stable"`. Any `flutter_version.json` whose channel is anything else SHALL fail `cue vet`.
|
||||
|
||||
The experience context is the CI engineer running schema validation locally (or via the `build.yml` validation step) — they get an immediate, loud failure if a non-stable release leaks into the manifest.
|
||||
|
||||
#### Scenario: Non-stable channel fails validation
|
||||
|
||||
- **GIVEN** a `flutter_version.json` with `flutter.channel == "beta"`
|
||||
- **WHEN** `cue vet config/schema.cue -d '#FlutterVersion' config/flutter_version.json` runs
|
||||
- **THEN** the command exits non-zero with a constraint-violation error on `flutter.channel`
|
||||
|
||||
#### Scenario: Schema itself is well-formed
|
||||
|
||||
- **GIVEN** the current `config/schema.cue`
|
||||
- **WHEN** `cue vet config/schema.cue -d '#FlutterVersion' config/flutter_version.json` runs against the committed `config/flutter_version.json`
|
||||
- **THEN** the command exits 0
|
||||
- **AND** no `reference "#PatchVersion" not found` (or any other undefined-reference) error is produced
|
||||
|
||||
### Requirement: A failed update run surfaces as a failed workflow
|
||||
|
||||
If any step in the update pipeline (release fetch, schema validation, build-tools lookup, PR creation) fails, the workflow SHALL exit non-zero so the CI engineer sees a red run in the Actions tab rather than a silent no-op.
|
||||
|
||||
The experience context is the on-call CI engineer scanning the repository's Actions tab — they need silent failures (e.g. "ran but did nothing") to be impossible for failure modes other than "no upstream change."
|
||||
|
||||
#### Scenario: Release fetch fails
|
||||
|
||||
- **GIVEN** `https://storage.googleapis.com/flutter_infra_release/releases/releases_linux.json` is unreachable
|
||||
- **WHEN** the scheduled run executes
|
||||
- **THEN** the `update_flutter_version` job fails (red ❌ in Actions tab)
|
||||
- **AND** no PR is opened
|
||||
|
||||
#### Scenario: Generated config fails schema validation
|
||||
|
||||
- **GIVEN** the workflow generated a `config/version.json` that violates `#Version`
|
||||
- **WHEN** `validate_config_version` runs
|
||||
- **THEN** the job fails
|
||||
- **AND** the `update_docs_and_create_pr` job is skipped, so no PR is opened
|
||||
|
||||
#### Scenario: A "no upstream change" run is green, not red
|
||||
|
||||
- **GIVEN** the upstream stable Flutter version equals the pinned version
|
||||
- **WHEN** the scheduled run executes
|
||||
- **THEN** the workflow finishes with status success (green ✅)
|
||||
- **AND** the only completed job is `update_flutter_version`
|
||||
@@ -0,0 +1,46 @@
|
||||
## 1. Schema and validation fixes (independent, ship-able alone)
|
||||
|
||||
- [x] 1.1 In `config/schema.cue`, replace the dangling `#PatchVersion` reference inside `#FlutterVersion.flutter` with `#SemverPatch`.
|
||||
- [x] 1.2 Confirm `config/schema.cue` already restricts `flutter.channel` to literal `"stable"` (no further change needed; verify only).
|
||||
- [x] 1.3 Run `cue vet config/schema.cue -d '#FlutterVersion' config/flutter_version.json` locally — must exit 0.
|
||||
- [x] 1.4 Run `cue vet config/schema.cue -d '#Version' config/version.json` locally — must exit 0.
|
||||
- [x] 1.5 In `config/android.cue`, change the `commandTests` length guard from `if len(input.fileContentTests) >= 3` to `if len(input.commandTests) >= 3`.
|
||||
|
||||
## 2. Replace JS fetcher with CUE-driven step
|
||||
|
||||
- [x] 2.1 In `.github/workflows/update_version.yml`, delete the `Update latest Flutter version` step (the `actions/github-script` call to `script/updateFlutterVersion.js`).
|
||||
- [x] 2.2 Rewrite the `Fetch and update latest Flutter version` step (id: `update_flutter_version`) to: (a) `curl` `releases_linux.json`; (b) `cue import` it; (c) read the on-disk old version via `cue export config/flutter_version.json --out json | jq -r .flutter.version` (or `cue eval` equivalent); (d) compute the latest stable version via `cue eval --concrete --expression '[for r in releases if r.channel == "stable" && (r.version =~ "^[0-9]+\\.[0-9]+\\.[0-9]+$") {r}][0]'` (verify shape during implementation); (e) compare; (f) only if different, run `cue eval --force --outfile config/flutter_version.json --concrete --expression ...` and `echo "result=true" >> $GITHUB_OUTPUT`; otherwise `echo "result=false" >> $GITHUB_OUTPUT`.
|
||||
- [x] 2.3 Echo old version, new version, and verdict to the run log before writing `$GITHUB_OUTPUT`.
|
||||
- [x] 2.4 Keep the existing `Validate version.json with CUE` and `Upload artifact with the new Flutter version` steps; verify their `if: steps.update_flutter_version.outputs.result == 'true'` gates still resolve correctly after step renames.
|
||||
- [x] 2.5 Delete `script/updateFlutterVersion.js`.
|
||||
- [x] 2.6 Search the repo for any remaining reference to `updateFlutterVersion.js` or `updateFlutterVersion` and remove it (none expected outside the workflow).
|
||||
|
||||
## 3. Wire build-tools sourcing from packages.txt
|
||||
|
||||
- [x] 3.1 Read `script/update_test.sh` and `script/updateAndroidVersions.gradle.kts` to determine where `android_sdk_build_tools_version` is currently sourced and how it flows into `config/version.json`.
|
||||
- [x] 3.2 In `update_version.yml` job `update_android_version`, give the `Update Android SDK build tools version` step `id: build_tools` and write the extracted version to `$GITHUB_OUTPUT` (the script already does this; just add the `id`).
|
||||
- [x] 3.3 Replace the gradle-derived build-tools value in `script/update_test.sh` (or wherever it's consumed) with the `packages.txt`-sourced value from step 3.2 — exposed via env var or step-output reference.
|
||||
- [x] 3.4 If `updateAndroidVersions.gradle.kts` no longer needs to extract build-tools, simplify it; if other Android values still need gradle extraction, leave gradle alone and only swap the build-tools field.
|
||||
- [x] 3.5 Verify that the resulting `config/version.json` still passes `cue vet config/schema.cue -d '#Version'` (the `#SemverPatch` constraint will reject malformed extractions).
|
||||
|
||||
## 4. Fix the commit-message step
|
||||
|
||||
- [x] 4.1 In `.github/workflows/update_version.yml` job `update_docs_and_create_pr`, change `Create commit message variable` to write `commit_message=...` to `$GITHUB_OUTPUT` instead of `$GITHUB_ENV`.
|
||||
- [x] 4.2 Echo the resolved commit message to the run log so an empty value is visible.
|
||||
- [x] 4.3 Confirm `peter-evans/create-pull-request` references resolve to the non-empty step output.
|
||||
|
||||
## 5. Cosmetic and stale cleanup
|
||||
|
||||
- [x] 5.1 Remove the stray empty `#` comment line in the `permissions:` block of job `update_flutter_version` (above `contents: write`).
|
||||
- [x] 5.2 Verify no unreferenced env vars or step ids remain (e.g. orphan `COMMIT_MESSAGE` env, unused job outputs).
|
||||
|
||||
## 6. End-to-end verification before merge
|
||||
|
||||
- [ ] 6.1 Push branch and trigger `update_version.yml` via `workflow_dispatch`. Inspect logs to confirm: old/new version echo, verdict, and outcome (PR opened OR clean skip).
|
||||
- [ ] 6.2 Manually rewind `config/flutter_version.json` on the branch to a known older version, push, re-trigger, and confirm an upgrade PR is opened with non-empty title and commit message.
|
||||
- [ ] 6.3 Restore `config/flutter_version.json` to current.
|
||||
- [ ] 6.4 Confirm `build.yml` still passes (it already references `config/schema.cue`).
|
||||
|
||||
## 7. Spec sync at archive time
|
||||
|
||||
- [ ] 7.1 Once shipped, archive the change so `openspec/specs/flutter-version-update/spec.md` reflects the as-built behavior (handled by `/opsx:archive`).
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-05-09
|
||||
@@ -0,0 +1 @@
|
||||
passed
|
||||
@@ -0,0 +1,133 @@
|
||||
## Context
|
||||
|
||||
After `adopt-gx-for-actions` (#439), the repository has three coordinated artifacts: `.github/gx.toml` (manifest), `.github/gx.lock` (resolved versions and SHAs), and `.github/workflows/gx.yml` (CI lint + tidy on PR). However, the existing `.github/renovate.json` still drives version bumps using Renovate's built-in `github-actions` manager, which edits the workflow YAML directly. The current `gx.yml` `tidy` job then runs and pushes a follow-up commit to sync `.github/gx.lock`. This works, but:
|
||||
|
||||
1. Manifest specifiers like `"^6"` and `"~0.3.0"` are not honored by Renovate — it walks past majors freely.
|
||||
2. Two systems edit overlapping files; cross-PR races and noisy chase-commits are routine.
|
||||
3. The "what versions are allowed" question has no canonical answer — `gx.toml` describes one constraint, Renovate operates on a different one, the lock records the resolution.
|
||||
|
||||
The repository is hosted on the **Mend Renovate App**, which forbids `postUpgradeTasks` (those require self-hosted Renovate with `allowedPostUpgradeCommands`). The single-commit-PR option from self-hosted Renovate is therefore unavailable. This design takes the next-best path: scope Renovate to a single file (`gx.toml`), and let CI propagate.
|
||||
|
||||
Verified beforehand:
|
||||
|
||||
- `gx tidy` resolves manifest specifier changes and rewrites both `gx.lock` and workflow `uses:` SHAs in a single run (`~/Code/gx/src/tidy/command.rs:97-148`). Phase 1 (`sync_manifest_actions`) only adds/removes manifest entries and does not overwrite a specifier already present, so a Renovate edit (`^6` → `^6.0.2`) survives Phase 1 untouched and is consumed by Phase 3 (`lock_sync`) and Phase 4 (`compute_workflow_patches`).
|
||||
- Renovate's built-in `github-actions` manager covers both `.github/workflows/**` and `.github/actions/**` (https://docs.renovatebot.com/modules/manager/github-actions/), so disabling it must be done by manager name, not file pattern.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Make `.github/gx.toml` the only Renovate-editable file for GitHub Actions versions.
|
||||
- Honor manifest specifiers (`^6`, `~0.3.0`) in Renovate's upgrade proposals so cross-major bumps require a human commit.
|
||||
- Keep the existing `gx.yml` `tidy` job as the sole agent that rewrites `gx.lock` and workflow `uses:` references.
|
||||
- Preserve the current monthly schedule (`* 0-3 1 * *`) for action upgrades.
|
||||
- Preserve all existing non-actions Renovate rules (Dockerfile suite tracking, npm grouping, weekly cadence, etc.).
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Switching off the Mend Renovate App or migrating to self-hosted Renovate.
|
||||
- Introducing `postUpgradeTasks` (incompatible with Mend App).
|
||||
- Changing `gx`, `gx.yml`, or any workflow file behavior.
|
||||
- Changing the schedule cadence or grouping for non-actions dependencies.
|
||||
- Adding a new scheduled `gx upgrade` cron workflow (option A from the prior research; rejected here in favor of letting Renovate continue to be the upgrade trigger).
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1: Use a Renovate `customManagers` regex over `gx.toml`, not Renovate's TOML manager
|
||||
|
||||
Renovate has no first-class manager for `gx.toml`. Two options:
|
||||
|
||||
- **Custom regex manager (chosen)**: One `customManagers` entry with a regex over `gx.toml`. Direct, debuggable, well-supported.
|
||||
- **Upstream a `gx` manager into Renovate**: A months-long external dependency. Out of scope.
|
||||
|
||||
The regex must extract:
|
||||
|
||||
- `depName` — full string from the manifest line (e.g., `github/codeql-action/upload-sarif`). Used in PR titles, dashboards, changelogs.
|
||||
- `packageName` — first two slash-separated segments only (`github/codeql-action`). Used by the `github-tags` datasource to query the right repo. The non-capturing group `(?:/[^"]+)?` swallows the optional subpath.
|
||||
- `currentValue` — the specifier (`^6`, `~0.3.0`).
|
||||
|
||||
Final pattern:
|
||||
|
||||
```
|
||||
"(?<depName>(?<packageName>[^/"]+/[^/"]+)(?:/[^"]+)?"\s*=\s*"(?<currentValue>[^"]+)"
|
||||
```
|
||||
|
||||
Datasource: `github-tags`. Versioning: `npm` (handles `^` and `~`). Extract template: `^v?(?<version>.+)$` (strip `v` prefix from tags so npm versioning compares cleanly).
|
||||
|
||||
Renovate docs (https://docs.renovatebot.com/configuration-options/#custommanagers) recommend "use only one method" per field but do not forbid two named captures from the same regex. We intentionally capture both `depName` and `packageName` because they differ for subpath actions; if Renovate rejects this combination at validation time, fall back to a `packageNameTemplate` Handlebars expression.
|
||||
|
||||
### Decision 2: Disable Renovate's built-in `github-actions` manager via `matchManagers`
|
||||
|
||||
Two options for stopping Renovate from editing workflow files:
|
||||
|
||||
- **`matchManagers: ["github-actions"]` + `enabled: false` (chosen)** — disables the manager by name. Clean, explicit, future-proof if Renovate adds new file patterns to the manager.
|
||||
- **`ignorePaths` covering `.github/workflows/**` and `.github/actions/**`** — file-pattern-based. Brittle if Renovate's coverage changes; also affects unrelated managers if any.
|
||||
|
||||
Manager-based disabling is the documented idiomatic path.
|
||||
|
||||
### Decision 3: Reuse the existing `gx.yml` `tidy` job; do not introduce a new workflow
|
||||
|
||||
The `tidy` job in `.github/workflows/gx.yml` already runs `gx tidy` on every PR (when not from a fork) and pushes the result via the `VERIFIED_COMMIT_*` GitHub App. After Path 2, that job's role expands from "sync the lock to match Renovate's workflow edits" to "resolve Renovate's manifest edit into lock + workflow rewrites". Both are well within `gx tidy`'s capability and the existing workflow's permission scope. No workflow changes are needed.
|
||||
|
||||
### Decision 4: Keep the schedule on the rule that targets `gx.toml`
|
||||
|
||||
The current `renovate.json` puts `schedule: ["* 0-3 1 * *"]` on the `github-actions` package rule. After this change, that rule is gone. The schedule moves to a new rule keyed on `matchFileNames: [".github/gx.toml"]`, preserving the monthly-first-day cadence. Grouping name `github-actions` is preserved so existing PR-routing rules (if any in branch-protection or auto-merge configs) continue to apply.
|
||||
|
||||
### Decision 5: Accept the two-commit PR shape
|
||||
|
||||
Each Renovate upgrade PR will contain:
|
||||
|
||||
1. Renovate's commit: 1-line edit in `gx.toml`.
|
||||
2. gx-bot's commit (via `grafana/github-api-commit-action`, App-token-signed): updated `gx.lock` + workflow files + composite actions.
|
||||
|
||||
A one-commit alternative requires `postUpgradeTasks`, which is unavailable on the Mend App. Two commits is acceptable: it matches the current observed PR shape (Renovate + chase-commit), and the "verified" App-token commit on top makes intent obvious in `git log`.
|
||||
|
||||
### Decision 6: Do not change `.github/gx.toml` or `gx.lock` as part of this proposal
|
||||
|
||||
This is a pure config refactor on the Renovate side. Manifest specifiers stay as currently committed. Future tightening of specifiers (e.g., switching `^6` to `~6.0.0` to forbid minor bumps) is a separate decision and a separate change.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **Risk**: The two-named-capture regex (`depName` + `packageName` in the same `matchStrings`) may fail Renovate's config validation or runtime extraction. → Mitigation: dry-run validation step in tasks (`renovate-config-validator`); fallback design uses a Handlebars `packageNameTemplate` instead.
|
||||
- **Risk**: `npm` versioning may misinterpret action tags that don't follow strict semver (e.g., `v6.0` instead of `v6.0.0`). → Mitigation: `extractVersionTemplate` strips the `v` prefix; if loose tags persist, Renovate will skip those bumps rather than corrupting them. `gx lint` would surface any resulting inconsistency.
|
||||
- **Risk**: Lint (in `gx.yml`) runs in parallel with `tidy` and may fail on the first commit (manifest ahead of workflows). The PR shows a transient red check that flips green after `tidy` pushes its commit. → Mitigation: behavior is identical to the current chase-commit pattern; documented in tasks; out-of-scope to restructure `gx.yml`.
|
||||
- **Risk**: An action used in workflows but missing from `gx.toml` would not be picked up by Renovate (since the only manager is now manifest-scoped). → Mitigation: `gx tidy` adds missing actions to the manifest on PR; `gx lint` flags unsynced manifest entries. A one-shot `gx tidy` locally before merging this proposal verifies completeness.
|
||||
- **Trade-off**: Loses Renovate's ability to identify exact patch-level upgrades for actions whose tags don't follow semver. The lock will still be regenerated against the manifest specifier whenever upstream publishes a tag in range — just not driven by a Renovate PR for non-semver-compliant tags. Acceptable for the action set in this repo.
|
||||
- **Trade-off**: Renovate's PR body will reference `depName` (full string with subpath); the github-tags lookup uses `packageName` (org/repo). The PR will list a `package: github/codeql-action`, dep: `github/codeql-action/upload-sarif`. Slight cosmetic asymmetry; not a correctness issue.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Local pre-flight: run `gx tidy` against the current tree; confirm zero diff. This validates that workflows, manifest, and lock are mutually consistent before changing Renovate.
|
||||
2. Locally validate the new `renovate.json` with the Renovate config validator and a `--dry-run=full` against the local checkout. Confirm Renovate finds `gx.toml`, parses each line, and resolves expected `currentVersion` for at least three sample actions (one with subpath, one with caret, one with tilde).
|
||||
3. Open a PR with the new `renovate.json`. The change is isolated to one file.
|
||||
4. Watch the next Mend Renovate cycle. Confirm:
|
||||
- No PRs editing `.github/workflows/**` or `.github/actions/**` are produced.
|
||||
- Any new PR edits only `.github/gx.toml`.
|
||||
- `gx.yml`'s `tidy` job adds the lock + workflow commit and `gx lint` passes on the head commit.
|
||||
|
||||
**Rollback**: revert the `renovate.json` change. Behavior returns to the prior (workflow-editing) Renovate model. No data migration is required because no other artifacts are touched.
|
||||
|
||||
## Automated Test Strategy
|
||||
|
||||
This change has no application code; verification is configuration-level and observational.
|
||||
|
||||
- **Critical path**: Renovate scans `gx.toml`, opens a PR with one `gx.toml` edit, `gx.yml` propagates lock + workflow updates, `gx lint` passes.
|
||||
- **Pre-merge verification**:
|
||||
- `npx --package renovate -- renovate-config-validator .github/renovate.json` — must pass.
|
||||
- Local Renovate dry-run (`renovate --platform=local --dry-run=full`) — must list expected upgrades for `gx.toml` entries and zero upgrades from the now-disabled `github-actions` manager.
|
||||
- Local `gx tidy` against the tree before opening the PR — must produce no diff (proves baseline consistency).
|
||||
- **Post-merge verification (first Renovate cycle, time-bounded)**: confirm one Renovate PR opens, edits only `gx.toml`, `gx.yml` `tidy` succeeds, and `gx lint` passes on the merged head.
|
||||
- **No new test infrastructure**. The existing `gx lint` job in `.github/workflows/gx.yml` is the on-going invariant check.
|
||||
|
||||
## Observability
|
||||
|
||||
- **Renovate-side failure surface**: Mend Renovate dashboard shows manager-extraction errors per repo. If the regex fails to match any line in `gx.toml`, Renovate's logs surface a warning and produce zero PRs for actions — visible as "no upgrades opened this cycle" on the dashboard. This is *silent* on the GitHub side; first observation is the absence of expected PRs after the cycle.
|
||||
- **gx-side failure surface**: `gx tidy` failures in the `gx.yml` `tidy` job appear as failed CI checks on the PR. `gx lint` failures appear as failed CI checks on every commit.
|
||||
- **Drift surface**: if a Renovate PR ever lands without the `tidy` follow-up commit (e.g., `tidy` job timed out, App token expired), `gx lint` fails on the PR head and merge is blocked. This is the load-bearing safety net.
|
||||
- **What is logged**: nothing new. `gx.yml` already prints tidy/lint progress; Renovate's logs are visible via the Mend dashboard.
|
||||
- **Can a failure be silent?**: yes — if Renovate's custom manager extracts zero matches, there is no GitHub-side signal until someone notices that monthly action upgrade PRs stopped arriving. Mitigation: the migration plan's local dry-run is the front-loaded check; running it before merge converts the silent failure into a loud, pre-merge one.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Does Renovate's config validator accept the two-named-capture regex (`depName` + `packageName` in the same `matchStrings`)? Tasks include the verification step; if validation fails, fall back to `packageNameTemplate` with a Handlebars conditional.
|
||||
- Should the `github-actions` group label be preserved on the new rule (`groupName: "github-actions"`) for any downstream branch-protection or auto-merge config? Default: yes, unless evidence emerges that no rule depends on it.
|
||||
@@ -0,0 +1,28 @@
|
||||
## Why
|
||||
|
||||
Today Renovate edits workflow files directly and `gx tidy` chases each PR with a fixup commit to sync `.github/gx.lock`. The two tools race on the same files, manifest specifiers (`^6`, `~0.3.0`) declared in `.github/gx.toml` are ignored, and Renovate can therefore propose a cross-major upgrade that the manifest was meant to forbid. Pointing Renovate at the manifest instead of the workflows turns `gx.toml` into the single source of truth for which action versions are allowed, and lets gx own propagation to the lock and workflow files.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Disable Renovate's built-in `github-actions` manager so it stops editing files under `.github/workflows/` and `.github/actions/`.
|
||||
- Add a Renovate `customManagers` regex entry that reads action specifiers from `.github/gx.toml` using the `github-tags` datasource and `npm` versioning (so `^6` and `~0.3.0` are honored).
|
||||
- Move the existing monthly schedule from the `github-actions` package rule to a new rule that targets `.github/gx.toml`.
|
||||
- Document that Renovate-driven action upgrades arrive as a `gx.toml`-only edit and are completed in-PR by the existing `gx.yml` `tidy` job.
|
||||
- **BREAKING** for the spec only: the requirement that Renovate PRs already carry the lock update on open is replaced by a requirement that `gx.yml`'s `tidy` job pushes the lock and workflow updates onto the Renovate PR branch before merge.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
_None._
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `actions-version-tracking`: the "Renovate-driven upgrades keep the lock in sync" requirement is replaced with one that scopes Renovate to `.github/gx.toml` and assigns lock + workflow propagation to `gx tidy` running in CI on the PR branch.
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected files: `.github/renovate.json` (rewrite), `openspec/specs/actions-version-tracking/spec.md` (delta applied during archive).
|
||||
- No code changes; no changes to `.github/workflows/gx.yml`, `.github/gx.toml`, or `.github/gx.lock`.
|
||||
- Operational impact: monthly Renovate PRs will edit one TOML line; the existing `gx.yml` `tidy` job adds a follow-up commit on the same PR with the lock and workflow updates. Net commit count per upgrade PR is unchanged or lower than today.
|
||||
- Safety property gained: Renovate cannot propose a major-version upgrade unattended. Crossing a major now requires a human to edit `gx.toml` (`^6` → `^7`), which becomes the review surface.
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Renovate-driven upgrades keep the lock in sync
|
||||
|
||||
Renovate SHALL be configured to manage GitHub Action versions exclusively through `.github/gx.toml`, not by editing workflow files. The repository's CI SHALL propagate every Renovate-driven `gx.toml` edit through to `.github/gx.lock` and the workflow files on the same pull request branch before merge, so that when the PR is merged the manifest, lock, and workflow `uses:` SHAs are mutually consistent.
|
||||
|
||||
#### Scenario: Renovate edits the manifest only
|
||||
|
||||
- **WHEN** Renovate opens an upgrade PR for a GitHub Action
|
||||
- **THEN** the only file modified by Renovate's commit is `.github/gx.toml`
|
||||
- **AND** the modification is a change to the action's specifier (e.g., `"^6.0.1"` → `"^6.0.2"`, or `"^6"` → `"^6"` with no change if already the broadest in-major specifier)
|
||||
|
||||
#### Scenario: gx.yml propagates the manifest edit on the PR branch
|
||||
|
||||
- **WHEN** a PR contains a `.github/gx.toml` edit and the workflow `uses:` SHAs do not yet match the new specifier
|
||||
- **THEN** the `gx.yml` workflow's `tidy` job runs `gx tidy`, regenerates `.github/gx.lock`, rewrites every affected `uses: <owner>/<repo>@<sha> # vX.Y.Z` reference in `.github/workflows/**` and `.github/actions/**`, and pushes the resulting changes onto the PR branch as a single commit
|
||||
- **AND** `gx lint` passes on the resulting head commit
|
||||
|
||||
#### Scenario: Manifest specifier bounds the upgrade
|
||||
|
||||
- **WHEN** a new major version of an action is published upstream and the manifest specifier is `^N` for the prior major
|
||||
- **THEN** Renovate does not propose an upgrade that crosses the major boundary
|
||||
- **AND** crossing the major requires a human commit that changes `.github/gx.toml` from `^N` to `^N+1`, after which `gx tidy` resolves the new major and rewrites lock and workflow files on the same PR
|
||||
|
||||
#### Scenario: Renovate's built-in github-actions manager is disabled
|
||||
|
||||
- **WHEN** Renovate evaluates this repository
|
||||
- **THEN** the built-in `github-actions` manager is disabled by configuration
|
||||
- **AND** no Renovate run produces a commit that edits any file under `.github/workflows/` or `.github/actions/`
|
||||
|
||||
#### Scenario: Lock drift is detected before merge
|
||||
|
||||
- **WHEN** for any reason a Renovate PR's head commit lacks the lock and workflow updates that match its `gx.toml` edit
|
||||
- **THEN** the `gx lint` CI job fails the PR
|
||||
- **AND** merge is blocked until `gx tidy` is run and committed
|
||||
@@ -0,0 +1,38 @@
|
||||
## 1. Pre-flight
|
||||
|
||||
- [x] 1.1 Run `gx tidy` against the current tree and confirm zero diff (proves the manifest, lock, and workflows are mutually consistent before changing Renovate)
|
||||
- [x] 1.2 Confirm Mend Renovate App is the bot in use by checking the author of the most recent Renovate PR
|
||||
|
||||
## 2. Rewrite `.github/renovate.json`
|
||||
|
||||
- [x] 2.1 Add a `packageRules` entry that disables Renovate's built-in `github-actions` manager (`matchManagers: ["github-actions"]` + `enabled: false`)
|
||||
- [x] 2.2 Remove the existing `github-actions` package rule (the one with `matchDatasources: ["github-tags"]` and the monthly schedule)
|
||||
- [x] 2.3 Add a `customManagers` regex entry targeting `^\\.github/gx\\.toml$` with named captures `depName`, `packageName`, `currentValue`, datasource `github-tags`, versioning `npm`, and `extractVersionTemplate: "^v?(?<version>.+)$"`
|
||||
- [x] 2.4 Add a new `packageRules` entry keyed on `matchFileNames: [".github/gx.toml"]` with `groupName: "github-actions"` and the existing monthly schedule `["* 0-3 1 * *"]`
|
||||
- [x] 2.5 Preserve the existing Dockerfile `customManagers` entry untouched
|
||||
- [x] 2.6 Preserve the existing `extends` array untouched
|
||||
- [x] 2.7 Apply Renovate's `fileMatch` → `managerFilePatterns` migration (regex wrapped in `/.../`) on both `customManagers` entries to silence the validator's deprecation notice on first run
|
||||
|
||||
## 3. Local validation
|
||||
|
||||
- [x] 3.1 Run `npx --package renovate -- renovate-config-validator .github/renovate.json` and confirm exit 0
|
||||
- [x] 3.2 Run `renovate --platform=local --dry-run=full` (with `LOG_LEVEL=debug`) and confirm: (a) zero upgrades from the `github-actions` manager, (b) `actions/checkout` is extracted with `packageName=actions/checkout` and a current version, (c) `github/codeql-action/upload-sarif` is extracted with `packageName=github/codeql-action`, (d) `plexsystems/container-structure-test-action` is extracted with the `~0.3.0` specifier
|
||||
- [x] 3.3 If validation fails on the two-named-capture regex, fall back to a `packageNameTemplate` Handlebars conditional that returns the first two slash-separated segments of `depName`, and re-run 3.1 and 3.2
|
||||
|
||||
## 4. Open the PR
|
||||
|
||||
- [ ] 4.1 Commit the `renovate.json` change on a topic branch and open a PR
|
||||
- [ ] 4.2 Confirm `gx.yml` `lint` and `tidy` jobs pass on the PR (no functional change yet, so they should be green)
|
||||
- [ ] 4.3 Merge after review
|
||||
|
||||
## 5. Post-merge observation (next Renovate cycle)
|
||||
|
||||
- [ ] 5.1 On the next monthly Renovate cycle, confirm any opened upgrade PR edits only `.github/gx.toml`
|
||||
- [ ] 5.2 Confirm `gx.yml`'s `tidy` job pushes a follow-up commit on the PR branch with `gx.lock` and workflow updates
|
||||
- [ ] 5.3 Confirm `gx lint` passes on the head commit of the Renovate PR
|
||||
- [ ] 5.4 If no Renovate PR appears within the expected window, check the Mend dashboard for manager-extraction errors against `gx.toml` and resolve before archiving
|
||||
|
||||
## 6. Archive
|
||||
|
||||
- [ ] 6.1 Run `/opsx:verify` to validate that the shipped behavior matches the specs
|
||||
- [ ] 6.2 Run `/opsx:archive` to fold the spec delta into `openspec/specs/actions-version-tracking/spec.md`
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-05-10
|
||||
@@ -0,0 +1 @@
|
||||
passed
|
||||
@@ -0,0 +1,81 @@
|
||||
## Context
|
||||
|
||||
The repo's user-facing docs are limited to `readme.md` (compiled from `docs/src/readme.mdx` via `docs/src/compile.js`) plus `docs/contributing.md` and `docs/windows.md`. There is no architectural overview, no Q&A surface, and no auto-update story. Cognition AI's hosted DeepWiki indexes any public GitHub repo for free, generates an architecture-aware wiki with diagrams and a chat interface, and — when the repo carries a DeepWiki badge — re-indexes automatically as `main` evolves. The wiki is reached by replacing `github.com` with `deepwiki.com` in the repo URL.
|
||||
|
||||
This proposal opts the repo into that hosted service. It does not stand up a docs site, replace the existing MDX pipeline, or send any code to a third party that doesn't already read it from public GitHub.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Give README readers a one-click path to an AI-generated wiki for the repo.
|
||||
- Keep the wiki current without any maintainer action after merge.
|
||||
- Keep the existing MDX→MD docs pipeline intact and authoritative for committed markdown.
|
||||
|
||||
**Non-Goals:**
|
||||
- Self-hosting DeepWiki or any OSS variant (deepwiki-open, OpenDeepWiki, RepoWiki). The hosted free tier is sufficient for a public repo of this size.
|
||||
- Replacing the MDX docs pipeline or migrating to Mintlify/Docusaurus/MkDocs.
|
||||
- Wiring the DeepWiki MCP server into shared tooling. Individual contributors can register `https://mcp.deepwiki.com/mcp` in their own Claude Code config; that is out of scope for this repo change.
|
||||
- Authoring a `.devin/wiki.json` outline up front. The default DeepWiki outline is the baseline; a steering file is a follow-up that should be motivated by an observed gap, not by speculation.
|
||||
- Generating reference docs from Dockerfiles or scripts.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Use hosted DeepWiki, not a self-hosted alternative
|
||||
|
||||
DeepWiki hosted (`deepwiki.com`) is free for public repos, is already operating against this repo's public GitHub URL, and requires zero infra. Self-hosted alternatives (deepwiki-open, OpenDeepWiki, RepoWiki) would require provisioning a server, paying for LLM tokens, and maintaining another service — all to reproduce a free capability. The trade is vendor lock-in to Cognition AI's roadmap, which is acceptable because the integration surface is one badge URL that any replacement could honor or be re-pointed away from.
|
||||
|
||||
Source: <https://docs.devin.ai/work-with-devin/deepwiki>, <https://cognition.ai/blog/deepwiki>.
|
||||
|
||||
### Source the badge in `docs/src/badges.mdx`, not directly in `readme.md`
|
||||
|
||||
`readme.md` is auto-generated and carries the comment `<!--- This markdown file was auto-generated from "readme.mdx" -->`. Editing it directly would be undone by the next compile. The MDX source for the badge row already lives at `docs/src/badges.mdx`. Adding the DeepWiki badge there flows it into the recompiled README and keeps the existing pipeline as the single source of truth.
|
||||
|
||||
### Defer `.devin/wiki.json` until a real gap is observed
|
||||
|
||||
DeepWiki accepts a `.devin/wiki.json` with `repo_notes` and a `pages` outline that overrides the default page structure. For a repo this small with self-evident structure (clearly named Dockerfiles, scripts, workflows, MDX docs), the default outline is expected to be adequate. Authoring a `pages` list before seeing what DeepWiki produces is speculative, adds a `.devin/` directory contributors must understand, and creates a config that can drift from the codebase. The cheaper path is: ship the badge, observe the generated wiki, then add steering only for the specific gaps that materialize. Adding the file later is a one-PR follow-up with no rework cost.
|
||||
|
||||
### Trust badge-presence as the auto-refresh trigger
|
||||
|
||||
DeepWiki refreshes wikis automatically for repos that carry a DeepWiki badge in their README. The repo already gets DeepWiki-generated content because it's public; the badge is what unlocks the auto-refresh path. No CI hook, no webhook, no scheduled workflow is needed.
|
||||
|
||||
Source: badge generator and behavior described at <https://deepwiki.ryoppippi.com/> and the DeepWiki dev guide.
|
||||
|
||||
### Use the standard Shields-style badge
|
||||
|
||||
The DeepWiki badge format used widely in the ecosystem is `https://deepwiki.com/badge.svg` linking to `https://deepwiki.com/<owner>/<repo>`. This matches the visual style of the other badges already in the README (OpenSSF Scorecard, channel, Docker version, Docker pulls) and avoids inventing a custom badge.
|
||||
|
||||
## Automated Test Strategy
|
||||
|
||||
This change has no runtime code path, so the verification surface is small and document-shaped. The critical path is: (1) the badge ends up in the regenerated `readme.md`, (2) the contributing doc gains the new section.
|
||||
|
||||
- **Build verification**: run the existing `npm run build` (or `npm run readme` + `npm run contributing`) inside `docs/src/` and confirm both regenerated files reflect the source changes. This is the same pipeline contributors already use; no new infrastructure.
|
||||
- **Manual smoke check**: open `https://deepwiki.com/gmeligio/flutter-docker-image` after merge and confirm the wiki renders. This is the user-visible outcome and cannot be automated against a third-party service.
|
||||
- **No new test infrastructure** is introduced.
|
||||
|
||||
## Observability
|
||||
|
||||
Failures are loud and shallow:
|
||||
- A broken badge URL would render as a broken-image icon in `readme.md` — visible immediately on GitHub.
|
||||
- A stale wiki (auto-refresh not firing) would surface to readers as out-of-date page content. Detection: spot-check after notable merges.
|
||||
|
||||
There is no silent-failure path that affects image users (the Docker images themselves are untouched). No new logs, metrics, or alerts are warranted.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[Vendor dependency on Cognition AI]** → Mitigation: integration surface is one badge URL. If Cognition discontinues the free tier we can remove the badge with no functional regression beyond the wiki page itself, or point the same badge URL at a self-hosted deepwiki-open deployment.
|
||||
- **[Generated content quality is outside maintainer control]** → Mitigation: the wiki is supplementary to the README, not load-bearing for image users. If the default outline is materially wrong, add `.devin/wiki.json` as a follow-up.
|
||||
- **[Auto-refresh cadence is undocumented]** → Mitigation: stale-by-a-few-hours is acceptable for a docs surface; no SLA is being promised to readers.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
Single-PR rollout, no data migration:
|
||||
1. Add the badge to `docs/src/badges.mdx` and recompile docs.
|
||||
2. Add the contributing section.
|
||||
3. Merge.
|
||||
4. Within DeepWiki's refresh window, the wiki at `deepwiki.com/gmeligio/flutter-docker-image` reflects the latest `main`.
|
||||
|
||||
Rollback: revert the PR. The wiki itself remains accessible (DeepWiki indexes public repos regardless), but auto-refresh stops and the badge disappears from the README. No image, CI, or release impact.
|
||||
|
||||
## Open Questions
|
||||
|
||||
None blocking. The DeepWiki refresh cadence is not publicly documented as a hard SLA, but that is an acceptable unknown for a supplementary docs surface.
|
||||
@@ -0,0 +1,26 @@
|
||||
## Why
|
||||
|
||||
CI engineers landing on the repo today only get a static README and two short markdown files. There is no way to ask "how does the windows pipeline cache the SDK?" or "what changed in the android Dockerfile recently?" without grepping the source. Hosted DeepWiki indexes any public GitHub repo for free, generates an architecture-aware wiki, and auto-refreshes when the repo carries a DeepWiki badge — so a one-time integration gives users a queryable, always-current doc surface at zero infra cost.
|
||||
|
||||
This passes the relevance gate: the badge and the linked wiki are visible to anyone reading the README, and the wiki itself is the experience the CI engineer notices when they click through.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add a DeepWiki badge to the README header so users can reach `deepwiki.com/gmeligio/flutter-docker-image` in one click. Source the badge in `docs/src/badges.mdx` so the existing MDX→MD compile carries it into `readme.md`.
|
||||
- Document the integration in `docs/src/contributing.mdx` (recompiled into `docs/contributing.md`) so contributors know the wiki exists and that the badge in the README is what keeps it auto-refreshing.
|
||||
- Defer page-outline steering (`.devin/wiki.json`) until DeepWiki's default outline is observed — add it as a follow-up only if a load-bearing gap appears.
|
||||
- No CI changes, no new dependencies in the Dockerfiles, no changes to image contents.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `repository-wiki`: An always-current, AI-generated knowledge base for the repository, reachable from the README via a badge and refreshed automatically when the repo changes.
|
||||
|
||||
### Modified Capabilities
|
||||
<!-- None — existing specs (actions-version-tracking, flutter-version-update) are unrelated. -->
|
||||
|
||||
## Impact
|
||||
|
||||
- **Files modified**: `docs/src/badges.mdx`, `docs/src/contributing.mdx`, regenerated `readme.md` and `docs/contributing.md` via `docs/src/compile.js`
|
||||
- **External dependency**: Hosted DeepWiki service (`deepwiki.com`) — free for public repos, owned by Cognition AI. No code or secrets sent; DeepWiki reads the public repo directly.
|
||||
- **No impact on**: Docker images, CI workflows, release pipeline, version bump scripts, end-user `docker run` UX.
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: README exposes a DeepWiki entry point
|
||||
|
||||
The README SHALL display a DeepWiki badge in the header badge row that links to `https://deepwiki.com/gmeligio/flutter-docker-image`, so a CI engineer reading the repo on GitHub or Docker Hub can reach the AI-generated wiki in one click.
|
||||
|
||||
The badge SHALL be authored in `docs/src/badges.mdx` (the source for the existing MDX→MD pipeline) so a recompile keeps `readme.md` in sync.
|
||||
|
||||
#### Scenario: Reader on GitHub clicks through to the wiki
|
||||
|
||||
- **GIVEN** a CI engineer is reading `readme.md` on GitHub
|
||||
- **WHEN** they click the DeepWiki badge in the header
|
||||
- **THEN** the browser navigates to the project's DeepWiki page
|
||||
- **AND** the page renders the auto-generated wiki for this repository
|
||||
|
||||
#### Scenario: Recompiling docs preserves the badge
|
||||
|
||||
- **GIVEN** the maintainer edits any source file under `docs/src/`
|
||||
- **WHEN** they run the docs compile script
|
||||
- **THEN** the regenerated `readme.md` still contains the DeepWiki badge in the header badge row
|
||||
|
||||
### Requirement: Wiki refreshes automatically when the repository changes
|
||||
|
||||
The repository SHALL opt into DeepWiki's badge-driven auto-refresh behavior so that the wiki a reader sees stays in step with the current state of `main` without any manual maintainer action.
|
||||
|
||||
The opt-in mechanism SHALL be the presence of the DeepWiki badge in `readme.md` (the same badge required above).
|
||||
|
||||
#### Scenario: Reader after a merge sees current content
|
||||
|
||||
- **GIVEN** a change has been merged to `main` that modifies `android.Dockerfile` or `windows.Dockerfile`
|
||||
- **WHEN** a reader visits the project's DeepWiki page after the next refresh cycle
|
||||
- **THEN** the wiki content reflects the merged change (no manual rebuild step required from the maintainer)
|
||||
|
||||
### Requirement: Contributors are told how the wiki works
|
||||
|
||||
`docs/contributing.md` (compiled from `docs/src/contributing.mdx`) SHALL include a short section pointing contributors at the DeepWiki and explaining that the badge in the README is what keeps the wiki fresh.
|
||||
|
||||
#### Scenario: New contributor opens contributing.md
|
||||
|
||||
- **GIVEN** a new contributor opens `docs/contributing.md`
|
||||
- **WHEN** they read through the document
|
||||
- **THEN** they find a section that names DeepWiki, links to the wiki URL, and explains that the README badge enables auto-refresh
|
||||
@@ -0,0 +1,17 @@
|
||||
## 1. README badge
|
||||
|
||||
- [x] 1.1 Add a DeepWiki badge to `docs/src/badges.mdx` linking to `https://deepwiki.com/gmeligio/flutter-docker-image`, using the same Shields-style markup as the surrounding badges
|
||||
- [x] 1.2 Run `npm run readme` (or `npm run build`) inside `docs/src/` to regenerate `readme.md`
|
||||
- [x] 1.3 Confirm the regenerated `readme.md` carries the DeepWiki badge in the header badge row and commit both files
|
||||
|
||||
## 2. Contributing docs
|
||||
|
||||
- [x] 2.1 Add a "Repository wiki" section to `docs/src/contributing.mdx` that names DeepWiki, links to `https://deepwiki.com/gmeligio/flutter-docker-image`, and explains that the README badge is what enables auto-refresh
|
||||
- [x] 2.2 Run `npm run contributing` to regenerate `docs/contributing.md`
|
||||
- [x] 2.3 Commit `docs/src/contributing.mdx` and `docs/contributing.md` together
|
||||
|
||||
## 3. Verification
|
||||
|
||||
- [ ] 3.1 Re-read the regenerated `readme.md` on GitHub's preview and confirm the DeepWiki badge renders and links correctly
|
||||
- [ ] 3.2 After merge to `main`, open `https://deepwiki.com/gmeligio/flutter-docker-image` and confirm the wiki renders
|
||||
- [ ] 3.3 Note any material gaps in the default outline; if found, file a follow-up to add `.devin/wiki.json`
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-05-09
|
||||
@@ -0,0 +1 @@
|
||||
passed
|
||||
@@ -0,0 +1,118 @@
|
||||
## Context
|
||||
|
||||
PR #339 has 11 commits over ~12 months and is currently in a state where the Windows CI job either fails the `COPY` step or never produces meaningful signal. The accumulated changes overlap three concerns: (1) fixing the test pipeline, (2) adding a Go-based dockertest harness, (3) renaming `script/test.sh` files. This change keeps only (1). It treats Pester running *inside* the test-target container as the single verification mechanism for the Windows image, mirroring how the Android image uses `container-structure-test`.
|
||||
|
||||
Constraints:
|
||||
|
||||
- `windows-2025` is the only viable runner. There is no Windows-container support in `docker/build-push-action`, no Buildx cache, and the full image build (Flutter clone + VS BuildTools install) takes 30–60 minutes per run.
|
||||
- The `gx`-managed action pinning regime (commit `846ffd6`, spec `actions-version-tracking`) requires every `uses:` to be SHA-pinned with a `# vX.Y.Z` comment. New actions added to `windows.yml` must go through `.github/gx.toml`.
|
||||
- `config/version.json` is the single source of truth for `flutter.version` (spec `flutter-version-update`). The Pester suite must read it, not hardcode.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- The `test_windows` PR check goes from "in_progress forever / red on COPY" to "green on a healthy image, red on a regression."
|
||||
- The Pester suite has at least one positive assertion on Flutter behavior (version, doctor) rather than only inspecting the on-disk VS package directories.
|
||||
- `test/windows/` contains exactly one form of test (Pester). No dead skeletons.
|
||||
- Everything that PR #339 added but did not finish is either finished or removed; nothing is left in a half-implemented state.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Publishing the `flutter-windows` image on tag (covered by `p2-release-windows-image`).
|
||||
- Tracking the VS BuildTools / Win11 SDK / CMake versions in `config/version.json` and through Renovate (covered by `p3-windows-version-schema`).
|
||||
- Reducing the Windows CI run time. The job will remain slow; this change accepts that.
|
||||
- Adding Docker Scout vulnerability scanning for the Windows image. The commented-out block is deleted; reintroducing it is left to a separate change if/when Scout becomes valuable for the Windows base image.
|
||||
- Adding the `validate_version` job to `windows.yml`. The same CUE validation runs in `build.yml`'s `validate_version_files` job already; duplicating it on `windows-2025` adds runner cost with no new signal.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision: Pester is the only verification harness; the Go/dockertest skeleton is deleted
|
||||
|
||||
The Go module under `test/windows/` (commit `df7666e`) is removed in this change. Reasons:
|
||||
|
||||
- It is not invoked by any CI workflow.
|
||||
- Its only useful assertion (the Pester `Exec` block in `main_test.go:38-49`) is commented out.
|
||||
- It runs the test image as `flutter-docker-image-windows-test:latest` without ever building it, so even uncommented it would fail.
|
||||
- Pester running *inside* the container is the natural fit: the assertions are about the file system and toolchain *of the container*, which is awkward to express through `dockertest.Exec` from a Linux Go process.
|
||||
|
||||
Alternatives considered:
|
||||
|
||||
- **Wire the Go harness into CI.** Rejected: doubles the test infrastructure for no new signal, and the harness would still need a Windows host to run Windows containers — the same `windows-2025` runner constraint.
|
||||
- **Keep the harness as a placeholder.** Rejected: dead code rots; unmaintained `go.mod` will collect `govulncheck` noise from Renovate.
|
||||
|
||||
### Decision: The Flutter version assertion reads `config/version.json` at test time, not via a build arg
|
||||
|
||||
The Pester test computes the expected version by parsing `config/version.json` (already `COPY`'d into the test stage as part of the `flutter` stage's checkout, or freshly copied in the `test` stage). Alternatives:
|
||||
|
||||
- **Hardcode the version in the test.** Rejected: drifts on every Flutter upgrade; defeats the point of `flutter-version-update`.
|
||||
- **Pass via `--build-arg expected_flutter_version` and bake into env.** Rejected: extra plumbing; the `flutter_version` build arg is already the source of truth fed to `git clone --branch`. Reading the manifest directly catches the case where someone passes a build arg that doesn't match the manifest.
|
||||
|
||||
### Decision: `flutter doctor` failure mode
|
||||
|
||||
`flutter doctor` produces lines like `[✓]`, `[!]`, `[✗]` (mapped from `ValidationType.success/partial/missing` in `packages/flutter_tools/lib/src/doctor_validator.dart`). The test applies a per-line rule based on the platform header:
|
||||
|
||||
- **Disabled platforms** (`Android`, `iOS`, `macOS`, `Linux`, `Web`, `Chrome`): skipped entirely. These are explicitly turned off by `flutter config --no-enable-*` so any marker on them is irrelevant.
|
||||
- **Owned-toolchain lines** (`Windows Version`, `Visual Studio - develop Windows apps`): fail unless the marker is `[✓]`. Both `[!]` and `[✗]` fail here. This is intentional: `WindowsVersionValidator` emits `[!]` when the Topaz OFD security module is detected (real build interference), and `VisualStudioValidator` emits `[!]` when VS is too old, needs reboot, has an incomplete install, is not launchable, is missing required components, or is missing the Windows 10 SDK — every one of which is a regression class this image must not ship. Sources: `packages/flutter_tools/lib/src/windows/{windows_version_validator,visual_studio_validator}.dart` in `flutter/flutter`.
|
||||
- **Other lines** (`Flutter`, `Connected device`, `Network resources`, etc.): fail only on `[✗]`. `[!]` here is informational (e.g., no devices connected), expected in a CI container.
|
||||
|
||||
The leaner "fail on `[✗]` only" rule was rejected: it would let a PR that drops `Microsoft.VisualStudio.Workload.VCTools` or `Windows11SDK.22621` from the Dockerfile pass with a `[!] Visual Studio` line, defeating the point of the smoke test.
|
||||
|
||||
### Decision: VS component pattern fix uses `,version=*`
|
||||
|
||||
The on-disk format for VS package directories is `<ComponentId>,version=<X.Y.Z.W>`. The current pattern `,versiona*` is a typo. The pattern `,version=*` is the minimum specific match that distinguishes a real install directory from any other coincident directory. Using `*` alone (no `,version=` anchor) would accept directories like `Microsoft.VisualStudio.Component.VC.CMake.Project_alt,…` which is too loose.
|
||||
|
||||
### Decision: `ENTRYPOINT` and `CMD` in the test stage both target `RunPester.ps1`
|
||||
|
||||
The `test` stage **resets** `ENTRYPOINT` to exec-form `["powershell", "-NoLogo", "-NoProfile", "-File"]` and sets `CMD` to `[".\\script\\RunPester.ps1"]`. This is required because the parent `flutter` stage uses a **shell-form** `ENTRYPOINT "C:\Users\ContainerUser\docker_entrypoint.ps1"` (the analytics-toggle script). Per Docker's documented `ENTRYPOINT`/`CMD` interaction, a shell-form `ENTRYPOINT` runs under PowerShell `-Command` and does **not** append `CMD` args — Docker emits the warning "Shell-form ENTRYPOINT and exec-form CMD may have unexpected results", and `docker run <image> .\script\RunPester.ps1` fails with `hcs::System::CreateProcess … 0x2 file not found` because the workflow's argument is treated as a separate executable.
|
||||
|
||||
With exec-form `ENTRYPOINT` in the test stage:
|
||||
|
||||
- `docker run <test-image>` invokes `powershell -NoLogo -NoProfile -File .\script\RunPester.ps1` (uses `CMD`).
|
||||
- `docker run <test-image> .\test\OtherTest.ps1` swaps in a different script (overrides `CMD`).
|
||||
- The CI workflow runs `docker run --rm <image>` with no explicit script argument; the `CMD` is the source of truth.
|
||||
|
||||
The analytics-toggle entrypoint inherited from the `flutter` stage is intentionally not preserved here — the test image doesn't need runtime analytics control, and the inherited shell-form is the bug source.
|
||||
|
||||
An earlier draft of this proposal kept the workflow's explicit `.\script\RunPester.ps1` arg "as redundant but harmless." That was wrong: it was the failure trigger when combined with the inherited shell-form `ENTRYPOINT`. The arg has been removed from the workflow.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[Risk] Build duration on `windows-2025` may exceed the GitHub Actions timeout for free-tier runners.** → Mitigation: this repo is not free-tier-constrained (see `release_android` already running multi-job pipelines). The `concurrency` block in `windows.yml` cancels stale runs so a force-push doesn't queue multiple builds. No further mitigation in this change; if duration becomes a blocker, layer caching in a follow-up.
|
||||
- **[Risk] `flutter doctor` output format is not a stable contract; Flutter could change `[✗]` to a different marker.** → Mitigation: the doctor parser is small and lives in the Pester test, so a Flutter upgrade that breaks it produces a single, localized red test rather than silent passes. The `flutter-version-update` spec already requires a passing CI before merge, so any format break is caught at upgrade time, not in production.
|
||||
- **[Trade-off] Removing the Go harness is a one-way door** for any future contributor who wants to add Linux-host-driven dockertest assertions. → Acceptable: such a future contributor can re-add the module deliberately, against a real requirement, instead of the current orphaned skeleton.
|
||||
- **[Trade-off] The `validate_version` and `scout-action` blocks are deleted rather than left commented.** → Acceptable: commented code that references a deleted file (`config/version.cue`) is misleading. Deletion forces the next iteration to think through what they actually need rather than uncomment dead code.
|
||||
|
||||
## Automated Test Strategy
|
||||
|
||||
This change is itself a test infrastructure change. Verification of the change works on two levels:
|
||||
|
||||
- **Self-test (the only level that matters for shipping)**: the `test_windows` job on PR #339 (or its replacement PR) goes green. That single check is the success criterion. It exercises every change in this proposal end-to-end: the `COPY` path is correct (build succeeds), the `versiona*` typo is fixed (VS-component test passes), the manifest-driven version test passes (Flutter version read from `config/version.json` matches what `flutter --version` reports), the doctor smoke test passes, the `CMD` is set (the workflow runs Pester and gets a non-zero exit on failure).
|
||||
- **No new test infrastructure**: Pester is already installed via `script/InstallPester.ps1`; no new tooling is added. The change is a *reduction* in tooling (Go module removed).
|
||||
|
||||
There is no unit-test layer below the Pester suite because the assertions are inherently integration-level — they require the real image to run. Local verification by contributors uses `docker compose run --rm windows-test` (which starts to work as part of this change).
|
||||
|
||||
## Observability
|
||||
|
||||
- **Failure surface**: every assertion is a Pester test. Pester emits per-test pass/fail with file:line in the workflow log. `Invoke-Pester -Configuration @{Output=@{Verbosity='Detailed'}}` (already configured in `script/RunPester.ps1`) shows the failing assertion's expected vs. actual.
|
||||
- **No silent failures possible**: `RunPester.ps1` ends with `Exit $LASTEXITCODE`, so any Pester test failure propagates to a non-zero `docker run` exit, which fails the workflow step. The `set -e`-equivalent for PowerShell (`$ErrorActionPreference = 'Stop'`) is already configured in the test stage's `SHELL` directive.
|
||||
- **Build-stage failures** (e.g., a future bad `COPY` path) surface as standard `docker build` errors with the failing instruction in the workflow log. There is no need for additional logging because the failing layer is named in the error.
|
||||
- **No telemetry sent off-platform**: GitHub Actions logs are the entire observability surface. Maintainers monitor `gh run list --workflow=windows.yml --limit 5` (or the PR check UI).
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Land this change on PR #339 (or replace #339 with a fresh PR built off the current `windows` branch).
|
||||
2. Force-push the branch after fixing the `COPY` and pattern, and confirm `test_windows` goes from "in_progress forever" to a green check.
|
||||
3. Delete the Go module files in the same commit as the Dockerfile fix; rerun the workflow to confirm no path now references `test/windows/main*.go`.
|
||||
4. Squash-merge PR #339 with a non-empty body referencing this proposal.
|
||||
5. No rollback is needed because every change is additive to the test surface or is a deletion of unused code; if the new Pester tests are wrong, they fail loudly and a follow-up fix applies — there is no production behavior to revert.
|
||||
|
||||
## Resolved Questions
|
||||
|
||||
- **Doctor `[!]` semantics on Windows-toolchain lines.** *Resolved 2026-05-10:* `[!]` on `Windows Version` and `Visual Studio - develop Windows apps` fails the test, same as `[✗]`. Captured in the "`flutter doctor` failure mode" decision above. Source: `WindowsVersionValidator` and `VisualStudioValidator` in `flutter/flutter`.
|
||||
- **`dart-flutter-telemetry.config` path resolution under `ContainerUser`.** *Resolved 2026-05-10:* The existing assertion path `$env:APPDATA\.dart-tool\dart-flutter-telemetry.config` is correct. `package:unified_analytics` (used by both `flutter` and `dart` CLIs) reads `Platform.environment['AppData']` on Windows and joins `.dart-tool/dart-flutter-telemetry.config`. The Dockerfile runs the disable-analytics commands as `ContainerUser` and the test runs as `ContainerUser`, so `$env:APPDATA` resolves to the same path in both phases. Sources: `pkgs/unified_analytics/lib/src/{utils,initializer,constants}.dart` in `dart-lang/tools`. No change needed.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should `windows.yml` upload Pester output as a workflow artifact (e.g., NUnit XML) for easier triage? *Tentatively no for this change* — the inline log is sufficient and adds no maintenance. Reopen if maintainers find themselves repeatedly digging through long Detailed-verbosity logs.
|
||||
- Should there be a smoke test that runs `flutter create` + `flutter build windows` end-to-end in the test stage? *Out of scope here* — the build is already exercised in the `flutter` stage via `flutter create build_app; flutter build windows;` (Dockerfile lines 64, 81). Re-running it inside the `test` stage would multiply build time without new signal. Revisit if a regression slips past the existing build step.
|
||||
@@ -0,0 +1,32 @@
|
||||
## Why
|
||||
|
||||
PR #339 ("ci: test windows image") has been open for ~12 months and still cannot turn green: `windows.Dockerfile` copies `./test/Windows.Tests.ps1` from a path that no longer exists (the file was moved to `./test/windows/` when the dockertest skeleton was added), and the Pester pattern for the `VC.CMake.Project` package has a typo (`,versiona*`) that would never match a real Visual Studio package directory. As a result the `flutter-windows` image has *zero* automated verification on every PR, while the `flutter-android` image runs `container-structure-test` and Docker Scout. This change is what's needed to actually land PR #339 and start producing a meaningful CI signal for the Windows image.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Fix `windows.Dockerfile` `COPY` to source `./test/windows/Windows.Tests.ps1` (the real path).
|
||||
- Fix `test/windows/Windows.Tests.ps1` `BeLikeExactly` pattern: `,versiona*` → `,version=*` to match the pattern actually written by `vs_BuildTools.exe`.
|
||||
- Add a Flutter version assertion that reads `config/version.json` and asserts `flutter --version` inside the container reports the same `flutter.version`. This converts the test job from "image builds" to "image is the version we shipped."
|
||||
- Add a `flutter doctor` smoke assertion that fails the test when doctor reports any error (warnings on platform-specific tooling are tolerated).
|
||||
- Set a default `CMD` in the `test` stage of `windows.Dockerfile` so `docker compose run windows-test` (and equivalent local invocations) actually runs Pester instead of exiting silently. The CI workflow continues to invoke `RunPester.ps1` explicitly.
|
||||
- Remove the dead `test/windows/main.go`, `test/windows/main_test.go`, `test/windows/go.mod`, `test/windows/go.sum`. The `ory/dockertest` harness was scaffolded in commit `df7666e` but never wired into CI, never builds the image it tries to run, and has its only useful assertion (the Pester `Exec`) commented out. Pester running inside the container is the chosen verification mechanism.
|
||||
- Either delete or wire up the two commented-out blocks in `.github/workflows/windows.yml`: the `docker/scout-action` step and the `validate_version` job (which still references the deleted `config/version.cue`). This change deletes them because Scout/version-validation parity is out of scope; the follow-up changes (`p2`, `p3`) reintroduce them deliberately.
|
||||
- Set a non-empty body on PR #339 describing the test surface.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `windows-image-testing`: defines what `.github/workflows/windows.yml` and `test/windows/Windows.Tests.ps1` are required to verify about the `flutter-windows` Docker image on every pull request — Flutter version match, doctor health, presence of pinned Visual Studio components, and analytics-disabled telemetry config.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
_None._ The Windows image previously had no spec; the existing `flutter-version-update` and `actions-version-tracking` specs are not touched.
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected files: `windows.Dockerfile`, `test/windows/Windows.Tests.ps1`, `.github/workflows/windows.yml`, `script/RunPester.ps1` (no change expected, but inputs change), `docker-compose.yml` (windows-test service still works).
|
||||
- Removed files: `test/windows/main.go`, `test/windows/main_test.go`, `test/windows/go.mod`, `test/windows/go.sum`.
|
||||
- No release/publish behavior changes — `release.yml` is untouched. Distribution of the Windows image is the explicit subject of `p2-release-windows-image`.
|
||||
- No version manifest changes — `config/schema.cue` is untouched. Tracking VS BuildTools / Win11 SDK / CMake versions in `config/version.json` is the explicit subject of `p3-windows-version-schema`.
|
||||
- Risk: the only CI signal here is a slow (`windows-2025`, multi-hour) Windows container build. This change does not address build duration; a green check is the success criterion, not a fast green check.
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Pull request CI verifies the Windows image on every PR
|
||||
|
||||
The `.github/workflows/windows.yml` workflow SHALL run on every `pull_request` event, build `windows.Dockerfile` with `--target test`, and run the Pester suite at `test/windows/Windows.Tests.ps1` inside that image. The workflow SHALL fail the PR check if the image build fails, if any Pester test fails, or if Pester exits non-zero.
|
||||
|
||||
The experience context is the maintainer reviewing a PR that touches `windows.Dockerfile`, `script/InstallPester.ps1`, `script/RunPester.ps1`, or `test/windows/**` — they get a single red/green check rather than having to build the multi-hour Windows image locally.
|
||||
|
||||
#### Scenario: PR check is green when the image is healthy
|
||||
|
||||
- **GIVEN** a PR whose `windows.Dockerfile` builds successfully on `windows-2025`
|
||||
- **AND** every Pester test in `test/windows/Windows.Tests.ps1` passes inside the resulting `test`-target image
|
||||
- **WHEN** the `test_windows` job runs
|
||||
- **THEN** the job exits 0
|
||||
- **AND** the `test_windows` check on the PR is reported as success
|
||||
|
||||
#### Scenario: PR check is red when a Pester test fails
|
||||
|
||||
- **GIVEN** a PR whose `test`-target image builds successfully
|
||||
- **AND** at least one Pester test fails (e.g., the Flutter version inside the image does not match `config/version.json`)
|
||||
- **WHEN** `script/RunPester.ps1` runs
|
||||
- **THEN** the script exits non-zero (it propagates `$LASTEXITCODE` from `Invoke-Pester`)
|
||||
- **AND** the `test_windows` job is reported as failed on the PR
|
||||
|
||||
#### Scenario: PR check is red when the Dockerfile cannot be built
|
||||
|
||||
- **GIVEN** a PR that breaks `windows.Dockerfile` (for example, by referencing a `COPY` source path that does not exist)
|
||||
- **WHEN** the `test_windows` job runs `docker build ... --target test`
|
||||
- **THEN** the build exits non-zero
|
||||
- **AND** the `test_windows` job is reported as failed on the PR
|
||||
|
||||
### Requirement: Tests assert the Flutter version inside the image matches `config/version.json`
|
||||
|
||||
The Pester suite SHALL include a test that runs `flutter --version` inside the running container and asserts the reported semver equals `flutter.version` from `config/version.json` at the commit being tested. The version SHALL be read from the manifest, not hardcoded in the test file.
|
||||
|
||||
The experience context is the CI engineer pulling `flutter-windows:<tag>` and expecting the in-container Flutter to match the tag — a silent drift between manifest and image is the failure mode this requirement prevents.
|
||||
|
||||
#### Scenario: Manifest and image agree
|
||||
|
||||
- **GIVEN** `config/version.json` declares `flutter.version == "X.Y.Z"`
|
||||
- **AND** the image was built with `--build-arg flutter_version=X.Y.Z`
|
||||
- **WHEN** the Flutter version Pester test runs
|
||||
- **THEN** `flutter --version` inside the container reports `Flutter X.Y.Z`
|
||||
- **AND** the test passes
|
||||
|
||||
#### Scenario: Manifest and image disagree
|
||||
|
||||
- **GIVEN** `config/version.json` declares `flutter.version == "X.Y.Z"`
|
||||
- **AND** the image was built with `--build-arg flutter_version=X.Y.W` (any other version)
|
||||
- **WHEN** the Flutter version Pester test runs
|
||||
- **THEN** the test fails with a message naming both versions
|
||||
|
||||
### Requirement: Tests assert `flutter doctor` reports a healthy Windows toolchain
|
||||
|
||||
The Pester suite SHALL include a test that runs `flutter doctor` inside the container and applies a per-line rule based on the platform header:
|
||||
|
||||
- Lines whose header is `Android`, `iOS`, `macOS`, `Linux`, `Web`, or `Chrome` SHALL be skipped (these platforms are explicitly disabled by `flutter config --no-enable-*` in `windows.Dockerfile`).
|
||||
- Lines whose header starts with `Windows Version` or `Visual Studio` SHALL fail the test unless the marker is `[✓]`. Both `[!]` (partial) and `[✗]` (missing) on these two lines indicate a real toolchain regression — `WindowsVersionValidator` and `VisualStudioValidator` in `flutter/flutter` emit `[!]` for conditions such as Topaz OFD interference, missing required VS components, missing Windows 10 SDK, incomplete install, or VS too old.
|
||||
- All other lines (e.g., `Flutter`, `Connected device`, `Network resources`) SHALL fail only on `[✗]`. `[!]` on these is informational in a CI container.
|
||||
|
||||
The experience context is the developer who runs `docker run flutter-windows flutter doctor` after pulling the image and expects a clean report for the Windows desktop toolchain — Pester catches regressions before the image is published.
|
||||
|
||||
#### Scenario: Doctor reports a clean Windows toolchain
|
||||
|
||||
- **GIVEN** the image was built successfully with VS BuildTools (CMake, Win11SDK, VCTools workload) installed
|
||||
- **WHEN** the doctor Pester test runs
|
||||
- **THEN** `flutter doctor` reports `[✓] Windows Version` and `[✓] Visual Studio - develop Windows apps`
|
||||
- **AND** the test passes
|
||||
|
||||
#### Scenario: Doctor reports a Windows-toolchain hard error
|
||||
|
||||
- **GIVEN** a PR that removes the `Microsoft.VisualStudio.Workload.VCTools` line from `windows.Dockerfile`
|
||||
- **AND** the image still builds (the workload removal does not break the build itself)
|
||||
- **WHEN** the doctor Pester test runs
|
||||
- **THEN** `flutter doctor` reports `[✗] Visual Studio` (or equivalent missing-toolchain marker)
|
||||
- **AND** the test fails
|
||||
|
||||
#### Scenario: Doctor reports a Windows-toolchain partial install
|
||||
|
||||
- **GIVEN** a PR that removes the `Microsoft.VisualStudio.Component.Windows11SDK.22621` line from `windows.Dockerfile`
|
||||
- **AND** the image still builds and Visual Studio itself is still present
|
||||
- **WHEN** the doctor Pester test runs
|
||||
- **THEN** `flutter doctor` reports `[!] Visual Studio - develop Windows apps` (partial: missing required component)
|
||||
- **AND** the test fails
|
||||
|
||||
#### Scenario: Doctor warning on a non-toolchain line is tolerated
|
||||
|
||||
- **GIVEN** the image was built successfully and `flutter doctor` reports `[!] Connected device` (no devices connected — expected in CI)
|
||||
- **WHEN** the doctor Pester test runs
|
||||
- **THEN** the `Connected device` line is classified as informational
|
||||
- **AND** the test passes
|
||||
|
||||
### Requirement: Tests assert presence of pinned Visual Studio components
|
||||
|
||||
The Pester suite SHALL assert that the directories at `$env:ProgramData\Microsoft\VisualStudio\Packages\` contain entries matching the components installed by `windows.Dockerfile`: `Microsoft.VisualStudio.Component.VC.CMake.Project`, `Microsoft.VisualStudio.Component.Windows11SDK.22621`, and `Microsoft.VisualStudio.Workload.VCTools`. The match pattern SHALL accept any installed `version=...` suffix.
|
||||
|
||||
The experience context is detecting silent removal or rename of a VS component in the Dockerfile — the package directory is the on-disk evidence that the component installed.
|
||||
|
||||
#### Scenario: All three components match
|
||||
|
||||
- **GIVEN** the image was built from the current `windows.Dockerfile`
|
||||
- **WHEN** the VS-component Pester tests run
|
||||
- **THEN** each of `VC.CMake.Project`, `Windows11SDK.22621`, and `Workload.VCTools` matches `*,version=*`
|
||||
- **AND** all three tests pass
|
||||
|
||||
#### Scenario: Pattern correctly accepts the on-disk format
|
||||
|
||||
- **GIVEN** a real package directory `Microsoft.VisualStudio.Component.VC.CMake.Project,version=17.13.35919.96`
|
||||
- **WHEN** the `BeLikeExactly` assertion runs against pattern `Microsoft.VisualStudio.Component.VC.CMake.Project,version=*`
|
||||
- **THEN** the assertion passes
|
||||
|
||||
### Requirement: Tests assert Flutter and Dart telemetry are disabled
|
||||
|
||||
The Pester suite SHALL assert that the dart-flutter telemetry config at `$env:APPDATA\.dart-tool\dart-flutter-telemetry.config` exists and contains `reporting=0`.
|
||||
|
||||
The experience context is the privacy-conscious user pulling the image and expecting analytics to be off by default — the test prevents a Dockerfile change from silently re-enabling telemetry.
|
||||
|
||||
#### Scenario: Telemetry is disabled
|
||||
|
||||
- **GIVEN** the image was built with `flutter config --no-analytics; dart --disable-analytics;` as currently in `windows.Dockerfile`
|
||||
- **WHEN** the telemetry Pester test runs
|
||||
- **THEN** `dart-flutter-telemetry.config` contains `reporting=0`
|
||||
- **AND** the test passes
|
||||
|
||||
### Requirement: The `test` Dockerfile stage is self-running by default
|
||||
|
||||
The `test` stage of `windows.Dockerfile` SHALL declare a `CMD` (or equivalent) that invokes `script/RunPester.ps1`, so that `docker run <test-image>` (and `docker compose run windows-test`) executes the Pester suite without requiring the caller to pass a command.
|
||||
|
||||
The experience context is the contributor who runs the test image locally — they should not need to know the exact PowerShell incantation to invoke Pester.
|
||||
|
||||
#### Scenario: Local invocation runs the suite
|
||||
|
||||
- **GIVEN** a test image built with `docker compose build windows-test`
|
||||
- **WHEN** the contributor runs `docker compose run --rm windows-test`
|
||||
- **THEN** Pester executes against `.\test`
|
||||
- **AND** the container exits with the Pester exit code
|
||||
|
||||
### Requirement: No dead Go/dockertest harness in `test/windows/`
|
||||
|
||||
The repository SHALL NOT contain a Go module under `test/windows/` unless that module is invoked by at least one CI job. The `ory/dockertest` skeleton (`main.go`, `main_test.go`, `go.mod`, `go.sum`) introduced in commit `df7666e` SHALL be removed because Pester running inside the container is the chosen verification mechanism.
|
||||
|
||||
The experience context is the contributor reading `test/windows/` and trying to determine which file is the source of truth — a dead harness alongside live Pester tests is a confusion hazard.
|
||||
|
||||
#### Scenario: Repository contains no orphan Go test files for Windows
|
||||
|
||||
- **WHEN** a contributor lists `test/windows/`
|
||||
- **THEN** the listing contains `Windows.Tests.ps1` (and any newly added Pester files)
|
||||
- **AND** the listing does not contain `main.go`, `main_test.go`, `go.mod`, or `go.sum`
|
||||
@@ -0,0 +1,50 @@
|
||||
## 1. Fix the broken Dockerfile copy and Pester typo
|
||||
|
||||
- [x] 1.1 In `windows.Dockerfile`, change the `COPY ./test/Windows.Tests.ps1` line to source `./test/windows/Windows.Tests.ps1`; keep the destination `.\test\Windows.Tests.ps1`.
|
||||
- [x] 1.2 In `test/windows/Windows.Tests.ps1`, change the CMake assertion pattern from `,versiona*` to `,version=*`. Apply the same `,version=*` form to the Win11SDK and VCTools assertions for consistency.
|
||||
|
||||
## 2. Make the test stage self-running
|
||||
|
||||
- [x] 2.1 In `windows.Dockerfile`, replace the trailing `# CMD Invoke-Pester ...` comment in the `test` stage with `CMD ["powershell", "-NoLogo", "-NoProfile", "-File", ".\\script\\RunPester.ps1"]` (or equivalent that invokes `RunPester.ps1`).
|
||||
- [x] 2.2 Verify locally that `docker compose run --rm windows-test` runs Pester and exits with the Pester exit code. (Skip if no Windows host available; rely on the CI run for confirmation.)
|
||||
|
||||
## 3. Add the Flutter version Pester test
|
||||
|
||||
- [x] 3.1 In `windows.Dockerfile`'s `test` stage, add `COPY ./config/version.json .\config\version.json` so the manifest is available at test time.
|
||||
- [x] 3.2 In `test/windows/Windows.Tests.ps1`, add a new `Describe "Flutter version"` block with a test that:
|
||||
- reads `config\version.json` via `Get-Content | ConvertFrom-Json`;
|
||||
- extracts `flutter.version`;
|
||||
- runs `flutter --version` and parses the first line into a semver string;
|
||||
- asserts the parsed version equals the manifest version, with a failure message naming both values.
|
||||
|
||||
## 4. Add the `flutter doctor` smoke test
|
||||
|
||||
- [x] 4.1 In `test/windows/Windows.Tests.ps1`, add a `Describe "Flutter doctor"` block that runs `flutter doctor` and captures stdout. At the top of `script/RunPester.ps1` (or in a `BeforeAll` for this Describe), force `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8` so the `[✓]`/`[!]`/`[✗]` glyphs survive PowerShell's default OEM encoding on `windows-2025`.
|
||||
- [x] 4.2 Implement a parser that classifies each line by its platform header and applies the per-line rule from the `flutter doctor` failure-mode decision in `design.md`:
|
||||
- **Skip** lines whose header is one of `Android`, `iOS`, `macOS`, `Linux`, `Web`, `Chrome` (intentionally disabled via `flutter config --no-enable-*`).
|
||||
- **Fail unless `[✓]`** for headers that start with `Windows Version` or `Visual Studio` (any `[!]` or `[✗]` here is a real toolchain regression — see the Flutter validator sources cited in `design.md`).
|
||||
- **Fail only on `[✗]`** for any other header (e.g., `Flutter`, `Connected device`, `Network resources`); `[!]` on these is informational in a CI container.
|
||||
- To survive encoding edge cases, match markers by character class (e.g., `^\[(✓|✔|!|✗|✘|x|X)\]`) rather than literal codepoints, mapping `✓/✔` → pass, `!` → partial, `✗/✘/x/X` → fail.
|
||||
- [x] 4.3 The test passes when at least the `Windows Version` and `Visual Studio - develop Windows apps` lines are tagged `[✓]` and no other non-skipped line is tagged `[✗]`.
|
||||
|
||||
## 5. Delete the dead Go/dockertest harness
|
||||
|
||||
- [x] 5.1 Delete `test/windows/main.go`, `test/windows/main_test.go`, `test/windows/go.mod`, `test/windows/go.sum`.
|
||||
- [x] 5.2 Confirm that no workflow under `.github/workflows/` still references Go or `dockertest` (`grep -r "dockertest\|go test\|go mod" .github/workflows/`).
|
||||
|
||||
## 6. Clean up commented-out workflow blocks
|
||||
|
||||
- [x] 6.1 In `.github/workflows/windows.yml`, delete the commented-out `Scan with Docker Scout` step block.
|
||||
- [x] 6.2 In `.github/workflows/windows.yml`, delete the commented-out `Push to Docker Hub` step block (release path is the subject of `p2-release-windows-image`).
|
||||
- [x] 6.3 In `.github/workflows/windows.yml`, delete the commented-out `validate_version` job block (it references the deleted `config/version.cue`).
|
||||
|
||||
## 7. Verify and ship
|
||||
|
||||
- [x] 7.1 Push the branch; wait for the `test_windows` job in `.github/workflows/windows.yml` to complete on `windows-2025`.
|
||||
- [x] 7.2 Confirm the job exits 0 with all Pester tests reporting `Passed`.
|
||||
- [x] 7.3 Update PR #339 (or open a replacement) with a non-empty body referencing this proposal: link to `openspec/changes/p1-fix-windows-ci-tests/proposal.md` and list the assertions now enforced.
|
||||
- [x] 7.4 Merge.
|
||||
|
||||
## 8. Archive
|
||||
|
||||
- [ ] 8.1 After merge, archive this change by running the `openspec-archive-change` flow so the `windows-image-testing` spec is promoted to `openspec/specs/windows-image-testing/spec.md`.
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-05-13
|
||||
@@ -0,0 +1 @@
|
||||
passed
|
||||
@@ -0,0 +1,34 @@
|
||||
## Why
|
||||
|
||||
`build.yml/test_image` builds a multi-GB three-stage Docker image (`flutter` → `fastlane` → `android`) and uses `cache-from/to: type=gha,mode=max` (build.yml:89-90). The GitHub Actions cache backend has a 10 GB per-repository quota with LRU eviction, and this image's `mode=max` cache routinely exceeds that — so on long-tail branches (or after another workflow churns the cache) builds fall back to cold and the step runs ~6 min instead of ~2½ min.
|
||||
|
||||
Docker's own CI guide recommends `type=registry` for images that don't fit comfortably in the GHA quota ([source](https://docs.docker.com/build/ci/github-actions/cache/)). With GHCR available (the repo already pushes release images there), the registry cache is free, has no GitHub-side eviction, and is shared across branches.
|
||||
|
||||
This change replaces the GHA cache backend with a GHCR registry cache and removes the eviction-induced cold-build tail.
|
||||
|
||||
## What Changes
|
||||
|
||||
- `build.yml/test_image` `cache-from` → `type=registry,ref=ghcr.io/${{ github.repository_owner }}/flutter-android:buildcache`.
|
||||
- `build.yml/test_image` `cache-to` → `type=registry,ref=ghcr.io/${{ github.repository_owner }}/flutter-android:buildcache,mode=max` — only on non-fork PRs and `workflow_dispatch` (fork PRs lack `packages: write`, so they fall back to `cache-from` only and skip `cache-to`).
|
||||
- Add `packages: write` to the `test_image` job permissions (already present for Scout's PR comment) — confirms scope.
|
||||
- Add GHCR login step alongside the existing Docker Hub login: `docker/login-action` with `registry: ghcr.io`, `username: ${{ github.actor }}`, `password: ${{ secrets.GITHUB_TOKEN }}`.
|
||||
- No change to `windows.yml` (separate image, separate concern — re-evaluate after this change lands).
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `ci-image-build-cache`: defines how the Flutter Docker image build SHALL persist and reuse layer cache across CI runs — backend, eviction behavior, fork-PR fallback, and the cache-ref naming contract.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
_None._
|
||||
|
||||
## Impact
|
||||
|
||||
- **Affected files**: `.github/workflows/build.yml:84-100` (the `Build image and push to local Docker daemon` step + new login step).
|
||||
- **Behavioral change**: cache hits become stable across branches and over time; `build` step median wall-clock drops from ~2m23s (current cache-hit case) to ~50s once the registry cache is warm. Cold builds (first run after this change merges) take the full ~6 min and prime the cache.
|
||||
- **GHCR storage**: `mode=max` registry cache for this image is ~3-5 GB and is overwritten in place on every push to the same tag (no growth). GHCR storage for public repos is free.
|
||||
- **Risk**: a corrupted cache push could fail subsequent builds. Mitigation: `cache-to` runs after the build succeeds; if the push itself fails, the build still passes (buildx treats cache export as best-effort).
|
||||
- **Out of scope**: changing the build itself, splitting Dockerfile stages into separately-cached images, caching on Windows runner.
|
||||
- **Relevance gate**: a CI engineer noticing the `Build image` step is consistently ~1 min would see the spec captures the contract (registry cache, branch-shared, fork-PR fallback) rather than re-discovering the rationale from a blog post.
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Build cache uses GHCR registry backend, not GHA cache
|
||||
|
||||
The Flutter Docker image build in `.github/workflows/build.yml` SHALL persist its layer cache to the GitHub Container Registry under a deterministic, branch-shared tag (`ghcr.io/<owner>/flutter-android:buildcache`) using `type=registry,mode=max`. The build SHALL NOT use `type=gha` for either `cache-from` or `cache-to`.
|
||||
|
||||
The experience context is the maintainer watching the `Build image` step on the PR check page — with `type=gha` and `mode=max`, eviction caused 20-30% of builds to fall back to cold (~6 min); with the registry backend, the cache is shared across branches and not subject to GHA's 10 GB quota.
|
||||
|
||||
#### Scenario: Non-fork PR populates the registry cache
|
||||
|
||||
- **GIVEN** a PR whose head branch lives in this repository (not a fork) and the `test_image` job runs
|
||||
- **WHEN** the build completes successfully
|
||||
- **THEN** the build pushes the cache manifest to `ghcr.io/<owner>/flutter-android:buildcache`
|
||||
- **AND** the next run of the same workflow (any branch) imports from that manifest
|
||||
|
||||
#### Scenario: Subsequent non-fork run hits the registry cache
|
||||
|
||||
- **GIVEN** the `buildcache` tag already exists from a prior successful run
|
||||
- **WHEN** a new `test_image` job runs
|
||||
- **THEN** the `Build image` step log contains `importing cache manifest from ghcr.io/<owner>/flutter-android:buildcache`
|
||||
- **AND** the `Build image` step completes in ≤ 90 seconds at the median across 10 consecutive cache-hit runs
|
||||
|
||||
### Requirement: Fork PRs read the registry cache but do not write to it
|
||||
|
||||
Fork PRs do not receive `packages: write` on the `GITHUB_TOKEN`, so `cache-to` SHALL be omitted for them. `cache-from` SHALL still reference the registry tag so fork builds get the warm-cache benefit, even though they cannot refresh the cache.
|
||||
|
||||
The experience context is a community contributor opening a PR from their fork — their build still benefits from the latest cache pushed by maintainer PRs, but they cannot pollute or invalidate the shared cache.
|
||||
|
||||
#### Scenario: Fork PR build reads but does not write the cache
|
||||
|
||||
- **GIVEN** a PR opened from a fork (`github.event.pull_request.head.repo.full_name != github.repository`)
|
||||
- **WHEN** the `test_image` job runs
|
||||
- **THEN** the `Build image` step uses `cache-from: type=registry,ref=ghcr.io/<owner>/flutter-android:buildcache` only
|
||||
- **AND** no `cache-to` value is passed to `docker/build-push-action`
|
||||
- **AND** the build succeeds even if the cache manifest is unavailable (cold-build fallback)
|
||||
|
||||
### Requirement: Registry cache tag does not grow unbounded
|
||||
|
||||
The cache tag `ghcr.io/<owner>/flutter-android:buildcache` SHALL be overwritten in place by `mode=max` exports — the manifest is replaced, not appended. The tag SHALL NOT grow more than 20% over its steady-state size across a rolling 7-day window of normal CI activity.
|
||||
|
||||
The experience context is the maintainer scanning GHCR storage costs (or quota usage on a private mirror) — `mode=max` is the cost-correct setting because it includes intermediate layers, but only as long as the manifest does not accumulate dead refs.
|
||||
|
||||
#### Scenario: Cache tag size after a week of normal CI
|
||||
|
||||
- **GIVEN** the `buildcache` tag has existed for ≥ 7 days under normal CI load (≥ 10 builds)
|
||||
- **WHEN** the size is sampled
|
||||
- **THEN** the size is within 20% of the size 7 days prior
|
||||
- **AND** no manual cleanup of the tag is required
|
||||
@@ -0,0 +1,17 @@
|
||||
## 1. Swap the cache backend in `build.yml`
|
||||
|
||||
- [x] 1.1 Add a `Login to GHCR` step to the `test_image` job, after `Login to Docker Hub`, gated `if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository` (fork PRs cannot push). Use `docker/login-action` with `registry: ghcr.io`, `username: ${{ github.actor }}`, `password: ${{ secrets.GITHUB_TOKEN }}`.
|
||||
- [x] 1.2 Replace `cache-from: type=gha` with `cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/flutter-android:buildcache` in `build.yml:89`.
|
||||
- [x] 1.3 Replace `cache-to: type=gha,mode=max` with `cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/flutter-android:buildcache,mode=max` in `build.yml:90`. Gate the value: only set it when the fork-PR predicate is true; otherwise omit `cache-to` entirely (use a YAML conditional or templated string). Cleanest implementation is a `cache-to` value of `${{ predicate && 'type=registry,...,mode=max' || '' }}`.
|
||||
- [x] 1.4 Confirm `permissions.packages: write` is set on the `test_image` job (already present for Scout) — satisfies spec scenario "Non-fork PR populates the registry cache".
|
||||
|
||||
## 2. Verify on a real PR before merge
|
||||
|
||||
- [x] 2.1 Push as a draft PR. Confirm the first `build.yml` run pushes the cache to `ghcr.io/.../flutter-android:buildcache` (visible under the Packages tab) and the second run hits it (visible in the `Build image` step log: `importing cache manifest from ghcr.io/...:buildcache`).
|
||||
- [x] 2.2 Open a PR from a fork (or simulate via a non-`packages:write` token). Confirm `cache-to` is not attempted and the build still succeeds, falling back to a cold build with the registry as `cache-from` only.
|
||||
- [x] 2.3 Record build-step durations from 3 consecutive runs in the PR description: cold (first), warm (second), warm (third). Compare against the pre-change median (~2m23s on cache-hit).
|
||||
|
||||
## 3. Post-merge closure check
|
||||
|
||||
- [x] 3.1 After 10 post-merge runs of `build.yml`, query `gh run list --workflow=build.yml --limit 20 --status completed` and confirm the median `Build image` step duration is ≤ 90s. If not, investigate whether the cache is being evicted or the `buildcache` tag is being overwritten by a parallel branch.
|
||||
- [x] 3.2 Confirm no growth in the `flutter-android:buildcache` tag size over the first week. `mode=max` rewrites the manifest in place; if size grows monotonically, an issue exists.
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-05-13
|
||||
@@ -0,0 +1 @@
|
||||
passed
|
||||
@@ -0,0 +1,90 @@
|
||||
## Context
|
||||
|
||||
The build today produces a local image (`load: true`) on the runner VM. To split testing and scanning into parallel jobs (p3), those jobs need a way to consume the same image bits without rebuilding. Three handoff mechanisms exist on GitHub-hosted runners:
|
||||
|
||||
1. **Registry push** — push to a tag the next job pulls. Standard, fast, but requires `packages: write` which fork PRs lack.
|
||||
2. **Artifact upload** — `docker save | gzip` → `upload-artifact` → `download-artifact` → `docker load`. Works for fork PRs but is slow for multi-GB images (~1-2 min upload + ~1-2 min download).
|
||||
3. **Self-hosted runner with shared storage** — out of scope; this repo uses GitHub-hosted runners.
|
||||
|
||||
Only (1) and (2) are viable. The contract has to handle both because the repo accepts fork PRs.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- A single, named handoff contract that downstream jobs (p3 test + scan) consume via job outputs.
|
||||
- Non-fork PRs use the fast path (registry); fork PRs use the artifact path automatically.
|
||||
- The handoff is idempotent: a re-run of the same PR overwrites the same `pr-N` tag (no garbage from re-runs).
|
||||
- Tag format is documented and stable so p4 can rely on it for cleanup.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Reorganizing the jobs (that's p3).
|
||||
- Optimizing the artifact path beyond `gzip` (~2 min handoff is acceptable for the fork-PR minority).
|
||||
- Cleaning up the tags (that's p4).
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1. Push and load from the same buildx run
|
||||
|
||||
**Decision**: Change the existing `docker/build-push-action` step from `load: true` to a multi-line `outputs:` that emits both a local docker image and a registry push:
|
||||
|
||||
```yaml
|
||||
outputs: |
|
||||
type=docker,name=<local-tag>
|
||||
type=registry,push=true,name=ghcr.io/<owner>/flutter-android:<handoff-tag>
|
||||
```
|
||||
|
||||
`load: true` and `push: true` are mutually exclusive shorthands for single-output `--output=type=docker` / `--output=type=registry` respectively, so the `outputs:` form is required when emitting both. Multi-output is a stable feature in buildx and BuildKit ≥ 0.13.0 (released Feb 2024) and is documented as first-class behavior. Order matters in one edge case (digest-pushed manifests, [discussion #1318](https://github.com/docker/build-push-action/discussions/1318)); place `type=docker` first to stay clear of it.
|
||||
|
||||
**Alternatives considered**:
|
||||
|
||||
- *Build with `push: true` only, then `docker pull` for the test step.* Rejected — adds a registry round-trip in the same job that already has the bits locally.
|
||||
- *Two sequential `build-push-action` invocations* (first `load: true`, second `push: true`), both backed by the registry cache from p1. The second is a near-full cache hit (~5-10s of buildkit overhead). Marginally simpler YAML, marginally more wall-clock. Equally acceptable; pick if multi-output ever misbehaves for this image.
|
||||
- *Build with `load: true`, then `docker tag` + `docker push` in a separate `run:` step.* Works but breaks the manifest-digest contract for any future multi-platform extension. Reject for that reason alone.
|
||||
|
||||
**Rationale**: One buildkit run emits both the local image (for the current serial test/scout) and the registry tag (for p3 consumers). Multi-output is no longer new (2+ years stable); the only nuance is exporter ordering.
|
||||
|
||||
### D2. Tag format: `pr-<number>` for PRs, `branch-<branch>` for `workflow_dispatch`
|
||||
|
||||
**Decision**: Tag = `pr-${{ github.event.pull_request.number }}` for `pull_request` events, `branch-${{ github.ref_name }}` for `workflow_dispatch`. Slashes in branch names are replaced with `-`.
|
||||
|
||||
**Alternatives considered**:
|
||||
|
||||
- *Use `github.run_id`.* Rejected — a new tag per re-run accumulates garbage that p4 has to chase down. PR number is stable across re-runs.
|
||||
- *Use `github.sha`.* Rejected for the same reason; a force-push generates a new tag.
|
||||
|
||||
**Rationale**: Stable per-PR tag means re-runs overwrite in place. p4's cleanup logic becomes "on PR close, delete `pr-<number>`" — a single-tag delete, no scanning required.
|
||||
|
||||
### D3. Fork PRs use `actions/upload-artifact` with gzipped `docker save`
|
||||
|
||||
**Decision**: When `github.event.pull_request.head.repo.full_name != github.repository`, skip the push and instead run `docker save <tag> | gzip > image.tar.gz` (≈ 5 GB → ~2 GB compressed for this image), then `actions/upload-artifact@v5` with `retention-days: 1` and a deterministic name `image-${{ github.run_id }}`.
|
||||
|
||||
**Alternatives considered**:
|
||||
|
||||
- *Refuse to handoff for fork PRs, run test+scout serially as today.* Rejected — fork PRs would lose the parallelization benefit from p3, which is exactly the slow case where contributors most want fast feedback.
|
||||
- *Use `docker save` without gzip.* Rejected — uncompressed save is ~5 GB, the upload is bandwidth-bound, and `gzip -1` saves ~50% in ~30s.
|
||||
- *Use `zstd`.* Slightly faster than gzip but the consumer (`docker load`) needs `zstd` installed; gzip is universal.
|
||||
|
||||
**Rationale**: 2 minutes of fork-PR slowdown is an acceptable price for the parallelization win in p3. The deterministic artifact name lets p3 download by name without scanning the artifact list.
|
||||
|
||||
### D4. Outputs schema: `image_ref` and `image_artifact`
|
||||
|
||||
**Decision**: The build job exposes two outputs:
|
||||
|
||||
- `image_ref`: full registry ref on non-fork; empty string on fork.
|
||||
- `image_artifact`: artifact name on fork; empty string on non-fork.
|
||||
|
||||
Consumers (p3) branch on which is non-empty.
|
||||
|
||||
**Alternatives considered**:
|
||||
|
||||
- *A single output `image` plus a `handoff_kind` discriminator (`"registry" | "artifact"`).* Equivalent. The two-output form is slightly more explicit in YAML and saves one `if` branch in the consumer.
|
||||
|
||||
**Rationale**: A consumer that misses the fork case fails at the `pull`/`download` step with a typed error, not silently runs on yesterday's image.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **R1**: GHCR rate-limiting on the push. Unlikely at this volume but documented as a future watch-item.
|
||||
- **R2**: Artifact storage cost for fork PRs. Retention 1 day caps total storage at ~2 GB × (active fork PRs in the last 24 h), well within free-tier limits for a public repo.
|
||||
- **R3**: Multi-output buildx is stable as of BuildKit 0.13.0 (Feb 2024) and is the documented way to combine `type=docker` and `type=registry`. Known edge case: ordering matters for digest-pushed manifests (place `type=docker` first). If multi-output ever misbehaves for this image, the documented fallback is two sequential build-push-action steps, accepting ~5-10s of buildkit overhead on the second (cached) run.
|
||||
@@ -0,0 +1,37 @@
|
||||
## Why
|
||||
|
||||
Today `test_image` builds the image with `load: true` (build.yml:88) into the local Docker daemon, then both `Test image` (container-structure-test) and `Scan with Docker Scout` consume that local image serially in the same job. There is no way for a downstream job to consume the freshly-built image without rebuilding it — the local daemon is per-runner-VM, and the build artifact is multi-GB.
|
||||
|
||||
This blocks **p3** (parallelize validation): to fan-out test + scan into separate jobs, those jobs need a shared place to pull the image from. This change introduces that handoff mechanism without yet reorganizing the jobs — it adds a push of the just-built image to a temporary GHCR tag (`pr-<number>` / `branch-<branch>` for `workflow_dispatch`), and exposes the tag as a job output. Fork PRs that cannot push to GHCR fall back to `actions/upload-artifact` with a `docker save` tarball.
|
||||
|
||||
This is intentionally a **no-behavior-change-yet** step: the existing `Test image` and `Scan with Docker Scout` steps still consume the locally-loaded image. Only after p3 lands do downstream jobs start pulling the handoff tag.
|
||||
|
||||
## What Changes
|
||||
|
||||
- In `build.yml/test_image`, after the existing `docker/build-push-action` step (which keeps `load: true`), add a second `docker/build-push-action` step (or change the existing one to `outputs: type=image,push=true` and let buildx tee both load + push from the same buildkit run) that pushes the image to `ghcr.io/${{ github.repository_owner }}/flutter-android:pr-${{ github.event.pull_request.number || github.run_id }}` — gated to non-fork PRs and `workflow_dispatch`.
|
||||
- For fork PRs, add a fallback step: `docker save ${{ image_tag }} | gzip > image.tar.gz` and `actions/upload-artifact` with `name: image-${{ github.run_id }}`, `retention-days: 1`. Fork-PR p3 jobs will `download-artifact` + `docker load`.
|
||||
- Expose two job outputs on `test_image` (which p3 will rename to `build_image`):
|
||||
- `image_ref`: the full registry ref (`ghcr.io/.../flutter-android:pr-N`) on non-fork; empty string on fork.
|
||||
- `image_artifact`: artifact name on fork; empty on non-fork.
|
||||
- Document the tag format in the spec so p4 (cleanup) and p3 (consumers) can rely on it.
|
||||
- This change does **not** yet remove the serial `Test image` and `Scan with Docker Scout` steps — those stay until p3.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `ci-image-handoff`: defines the contract by which a CI job that builds the Flutter Docker image makes that image available to other jobs in the same run — registry-tag handoff for non-fork PRs, artifact handoff for fork PRs, and the naming and output schema both paths SHALL satisfy.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
_None._
|
||||
|
||||
## Impact
|
||||
|
||||
- **Affected files**: `.github/workflows/build.yml:84-101` (the build step + new push/save step + outputs block on the job).
|
||||
- **Behavioral change**: the build now also produces a registry tag (non-fork) or artifact (fork). No effect on `Test image` or `Scout` until p3.
|
||||
- **GHCR storage**: each PR creates a temporary tag; p4 will clean these up on PR close. Until p4 lands, expect tags to accumulate at the rate of new PRs.
|
||||
- **Fork-PR slowdown**: the `docker save | gzip` + upload is ~1-2 min for a 5 GB image. This regresses fork-PR wall-clock by that amount, but only until p3 redeems the cost via parallelization.
|
||||
- **Risk**: a build that succeeds but fails the push leaves the job in an ambiguous state. Mitigation: the push uses `if: success()` and the job fails on push failure (do not `continue-on-error: true` — silently losing the handoff would break p3 consumers later).
|
||||
- **Depends on**: p1 (registry login is shared infra) — if p1 has not landed, this change adds its own `Login to GHCR` step.
|
||||
- **Out of scope**: cleaning up the tags (p4), consuming the tags (p3), Windows image handoff.
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Build job exposes a handoff for downstream jobs
|
||||
|
||||
The CI job that builds the Flutter Docker image SHALL expose three job outputs that downstream jobs in the same workflow run can consume to access the image without rebuilding it:
|
||||
|
||||
- `image_ref`: the full registry reference (`ghcr.io/<owner>/flutter-android:<tag>`) when the build pushed to GHCR; empty string otherwise.
|
||||
- `image_artifact`: the artifact name (`image-<run_id>`) when the build uploaded a `docker save` tarball instead; empty string otherwise.
|
||||
- `image_local_tag`: the tag the image carries in the local docker daemon (and inside the artifact tarball) — `flutter-android:<flutter-version>`. Always set, regardless of handoff channel.
|
||||
|
||||
Exactly one of `image_ref` and `image_artifact` SHALL be non-empty per run; `image_local_tag` SHALL always be non-empty. A consumer SHALL be able to decide its pull strategy from the outputs alone, without inspecting `github.event` itself.
|
||||
|
||||
The experience context is a maintainer adding a new validation step in a later change — they look at the build job's outputs, see exactly one handoff channel populated, and write a single consumer that branches on which channel. The `image_local_tag` output lets fork-path consumers reference the image by its loaded tag without recomputing it from the version manifest.
|
||||
|
||||
#### Scenario: Outputs encode the handoff kind unambiguously
|
||||
|
||||
- **GIVEN** any successful build run
|
||||
- **WHEN** the run completes
|
||||
- **THEN** exactly one of `image_ref` and `image_artifact` is non-empty
|
||||
- **AND** the non-empty one matches the documented format (`ghcr.io/<owner>/flutter-android:pr-<N>` / `ghcr.io/<owner>/flutter-android:branch-<branch>` or `image-<run_id>`)
|
||||
- **AND** `image_local_tag` is non-empty and matches `flutter-android:<flutter-version>`
|
||||
|
||||
### Requirement: Non-fork PRs and workflow_dispatch use the registry handoff
|
||||
|
||||
For events that have `packages: write` available on `GITHUB_TOKEN` (`pull_request` from a same-repo head or `workflow_dispatch`), the build SHALL push the image to a deterministic GHCR tag and set `image_ref` to the full registry ref.
|
||||
|
||||
The tag format SHALL be:
|
||||
|
||||
- `pr-${{ github.event.pull_request.number }}` for `pull_request` events.
|
||||
- `branch-${{ github.ref_name }}` (with `/` replaced by `-`) for `workflow_dispatch`.
|
||||
|
||||
The experience context is the p4 cleanup workflow operator — they need a tag pattern they can match-and-delete on PR close without scanning the registry.
|
||||
|
||||
#### Scenario: Non-fork PR pushes the handoff tag
|
||||
|
||||
- **GIVEN** a `pull_request` event with `github.event.pull_request.head.repo.full_name == github.repository`
|
||||
- **WHEN** the build job runs and completes successfully
|
||||
- **THEN** `ghcr.io/<owner>/flutter-android:pr-<N>` exists in GHCR with the just-built image
|
||||
- **AND** the job output `image_ref` equals that ref
|
||||
- **AND** the job output `image_artifact` is empty
|
||||
- **AND** the job output `image_local_tag` equals `flutter-android:<flutter-version>`
|
||||
|
||||
#### Scenario: Re-running a PR overwrites the same handoff tag
|
||||
|
||||
- **GIVEN** a PR whose build has already produced `pr-<N>` once
|
||||
- **WHEN** the workflow is re-run for the same PR
|
||||
- **THEN** the tag `pr-<N>` is overwritten in place (no `pr-<N>-2` or similar accumulation)
|
||||
- **AND** the prior image bits are eligible for garbage collection by GHCR's regular GC
|
||||
|
||||
### Requirement: Fork PRs use an artifact handoff
|
||||
|
||||
For `pull_request` events from a fork (where `packages: write` is not available), the build SHALL skip the registry push, save the image with `docker save | gzip`, upload it via `actions/upload-artifact` with retention ≤ 1 day, and set `image_artifact` to the artifact name.
|
||||
|
||||
The experience context is a community contributor opening a fork PR — their PR still gets the parallel-validation benefit from later changes (p3), even though the runner cannot push to GHCR.
|
||||
|
||||
#### Scenario: Fork PR uploads the image artifact
|
||||
|
||||
- **GIVEN** a `pull_request` event with `github.event.pull_request.head.repo.full_name != github.repository`
|
||||
- **WHEN** the build job runs and completes successfully
|
||||
- **THEN** an artifact named `image-<run_id>` exists for the run, containing `image.tar.gz`
|
||||
- **AND** the artifact retention is ≤ 1 day
|
||||
- **AND** the job output `image_artifact` equals `image-<run_id>`
|
||||
- **AND** the job output `image_ref` is empty
|
||||
- **AND** the job output `image_local_tag` equals `flutter-android:<flutter-version>` (the tag carried inside the tarball)
|
||||
|
||||
#### Scenario: Fork PR fallback succeeds even when GHCR is unreachable
|
||||
|
||||
- **GIVEN** a fork PR build
|
||||
- **WHEN** the build completes
|
||||
- **THEN** no GHCR push is attempted
|
||||
- **AND** the build does not fail due to missing `packages: write` permission
|
||||
@@ -0,0 +1,38 @@
|
||||
## 1. Add the GHCR push to the build step
|
||||
|
||||
- [x] 1.1 Reuse the existing `Login to GHCR` step at `build.yml:75-81` (landed via p1). Confirm its predicate (`github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository`) matches the fork-PR gate used in 1.3 — they must agree.
|
||||
- [x] 1.2 Compute the handoff tag in a shell step: `pr-${{ github.event.pull_request.number }}` for `pull_request`, `branch-${{ github.ref_name }}` (with `/` → `-`) for `workflow_dispatch`. Emit as `steps.handoff.outputs.tag`.
|
||||
- [x] 1.3 Compute the fork predicate as `steps.handoff.outputs.is_fork`: `github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository`.
|
||||
- [x] 1.4 Update the `docker/build-push-action` step to emit both a local docker image and a registry push when `is_fork == false`. Replace `load: true` with a multi-line `outputs:` (note: `load` and `push` shorthands are mutually exclusive, so neither can be set when `outputs:` is used):
|
||||
|
||||
```yaml
|
||||
outputs: |
|
||||
type=docker,name=${{ steps.metadata.outputs.tags }}
|
||||
type=registry,push=true,name=ghcr.io/${{ github.repository_owner }}/flutter-android:${{ steps.handoff.outputs.tag }}
|
||||
```
|
||||
|
||||
Place `type=docker` before `type=registry` (digest-ordering nuance, [discussion #1318](https://github.com/docker/build-push-action/discussions/1318)). For fork PRs (`is_fork == 'true'`), emit only `type=docker` so the existing serial test/scout still has a local image. Satisfies spec scenario "Non-fork PR pushes the handoff tag".
|
||||
|
||||
## 2. Add the fork-PR artifact fallback
|
||||
|
||||
- [x] 2.1 Add a step gated `if: steps.handoff.outputs.is_fork == 'true'` that runs `docker save <metadata.tags[0]> | gzip > image.tar.gz`.
|
||||
- [x] 2.2 Add an `actions/upload-artifact@v5` step gated on the same predicate, with `name: image-${{ github.run_id }}`, `path: image.tar.gz`, `retention-days: 1`, `compression-level: 0` (already gzipped).
|
||||
|
||||
## 3. Expose job outputs
|
||||
|
||||
- [x] 3.1 Add `outputs:` to the `test_image` job:
|
||||
- `image_ref: ${{ steps.handoff.outputs.is_fork == 'true' && '' || format('ghcr.io/{0}/flutter-android:{1}', github.repository_owner, steps.handoff.outputs.tag) }}`
|
||||
- `image_artifact: ${{ steps.handoff.outputs.is_fork == 'true' && format('image-{0}', github.run_id) || '' }}`
|
||||
- `image_local_tag: ${{ format('flutter-android:{0}', env.FLUTTER_VERSION) }}` — always set; the tag both the locally-loaded image and the `docker save` tarball carry.
|
||||
- Satisfies spec scenario "Outputs encode the handoff kind unambiguously".
|
||||
|
||||
## 4. Verify on a real PR before merge
|
||||
|
||||
- [x] 4.1 Push as a non-fork draft PR. Confirm the tag `ghcr.io/<owner>/flutter-android:pr-<N>` appears under GHCR Packages, the job output `image_ref` is populated, and the existing `Test image` and `Scout` steps still pass on the locally-loaded image.
|
||||
- [x] 4.2 Push from a fork (or simulate by gating the predicate to always-true for one run). Confirm the artifact `image-<run_id>` is uploaded (~2 GB), the output `image_artifact` is populated, and `image_ref` is empty.
|
||||
- [x] 4.3 Re-run the same PR. Confirm the existing `pr-N` tag is overwritten in place (no duplicate `pr-N-1`, `pr-N-2`, etc.) — satisfies spec scenario "Re-running a PR overwrites the same handoff tag".
|
||||
|
||||
## 5. Post-merge closure check
|
||||
|
||||
- [x] 5.1 After 5 post-merge PRs, list GHCR tags matching `pr-*` and confirm they accumulate (cleanup is p4, not this change).
|
||||
- [x] 5.2 Confirm fork-PR build wall-clock has regressed by ≤ 3 minutes vs. pre-change median — this is the expected cost until p3 redeems it.
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-05-09
|
||||
@@ -0,0 +1 @@
|
||||
passed
|
||||
@@ -0,0 +1,96 @@
|
||||
## Context
|
||||
|
||||
`release.yml` is the publishing workflow for tagged releases. It currently has five jobs, all Android-scoped: `release_android`, `update_description`, `record_image`, `set_bootstrap_image`, `create_github_release`. The Windows Dockerfile and the `windows.yml` PR-test workflow exist but are not connected to release. PR #339's `windows.yml` even has a commented-out `Push to Docker Hub` step (lines 84-88) hinting at this gap.
|
||||
|
||||
Constraints:
|
||||
|
||||
- Docker Buildx and `docker/build-push-action` cache features do not work for Windows containers (already noted in `windows.yml`'s comment "Docker Buildx is not supported for Windows containers"). The release job must use `docker build` + `docker push` directly, like `windows.yml` does.
|
||||
- The `gx`-managed pinning regime (spec `actions-version-tracking`) requires every new `uses:` to be SHA-pinned via `.github/gx.toml`. The actions themselves (`actions/checkout`, `docker/metadata-action`, `docker/login-action`, `actions/github-script`) are already pinned by `release_android`, so no new top-level `[actions]` entry is needed. However, `[actions.overrides]."docker/metadata-action"` is keyed per `(workflow, job, step)` and gains one new entry for the `release_windows` step, pinned at `~5.10.0` for parity with `release_android` (the `~5.7.0` entry for `windows.yml::test_windows` is unrelated and stays).
|
||||
- The three-registry fan-out (Docker Hub + GHCR + Quay) is not negotiable — it's the pre-existing distribution promise from `release_android`.
|
||||
- `windows-2025` runner cost: each release run adds 30–60 minutes of `windows-2025` minutes. The repository already pays for this in the PR-test workflow per `p1`, so this is incremental cost on tag pushes only.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- A single tag push publishes both `flutter-android:X.Y.Z` and `flutter-windows:X.Y.Z` to Docker Hub, GHCR, and Quay.
|
||||
- The Windows release path is *operationally identical* to the Android release path from a user's perspective: same registries, same tag scheme, same OCI labels.
|
||||
- Failure isolation between architectures: Windows runner flake does not block Android publishing.
|
||||
- `workflow_dispatch` continues to allow manual re-runs without re-tagging.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Updating Docker Hub description for the Windows image (`peter-evans/dockerhub-description`). The current `update_description` job is Android-scoped against `readme.md`. Adding a Windows variant requires a separate readme and a separate Docker Hub repo description; out of scope.
|
||||
- Recording Windows image vulnerabilities in Docker Scout (`record_image` job). Scout's Windows-base-image coverage is limited and the existing `windows.yml` already commented out the Scout block (line 65 onwards) for that reason. If/when Scout becomes useful for Windows, add it in a follow-up.
|
||||
- Updating the `FLUTTER_VERSION` repo variable from a Windows job (`set_bootstrap_image`). That variable bootstraps `test_gradle` against the Android image; Windows has no analogous bootstrap need today.
|
||||
- Including the Windows image in the GitHub Release notes generated by `create_github_release`. The release notes are tag-scoped, not image-scoped, so they continue to apply.
|
||||
- Multi-arch manifests (`linux/amd64` + `windows/amd64` under one tag). Windows containers cannot share a manifest list with Linux containers in practice; users pull the right image for their host. Status quo.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision: Add a single `release_windows` job, not a matrix on `release_android`
|
||||
|
||||
A `strategy.matrix` over `[android, windows]` would conflict on `runs-on` (`ubuntu-24.04` vs `windows-2025`), `dockerfile` (`android.Dockerfile` vs `windows.Dockerfile`), `target` (`android` vs `flutter`), and the `clean-runner-disk` step (Linux-only). The result would be a matrix mostly composed of conditionals — less readable than two parallel jobs sharing the metadata pattern. Two-job design wins for clarity.
|
||||
|
||||
Alternatives considered:
|
||||
|
||||
- **Reusable workflow** with the registry login + metadata + push pattern factored out. Rejected: only two callers (Android and Windows), and the steps differ enough (Buildx vs. plain docker build) that the reusable surface would have ~5 inputs to model two callers. Not worth it.
|
||||
|
||||
### Decision: No Buildx on the Windows job; use plain `docker build` + per-registry `docker push`
|
||||
|
||||
`docker/build-push-action` does **not** support Windows containers — the action runs through buildx/buildkit, and Windows-container support there has been an open feature request since 2020 (https://github.com/docker/build-push-action/issues/18). Buildkit's experimental WCOW worker (since v0.13.0, Nov 2024) requires manual containerd 1.7.7+, CNI, and admin-elevated setup that is not available out of the box on hosted `windows-2025` runners. Third-party alternatives (e.g., `mr-smithers-excellent/docker-build-push@v6`) work but consume `docker/metadata-action` outputs in a comma-delimited format that conflicts with OCI label values containing commas, and add a non-Docker-OSS dependency to the gx-managed supply chain.
|
||||
|
||||
The Windows job therefore uses plain `docker build` (multiple `-t` flags from `${{ steps.metadata.outputs.tags }}`, multiple `--label` flags from `${{ steps.metadata.outputs.labels }}`) followed by `docker push` per tag. This matches what `windows.yml::test_windows` already does on the same runner image. GHA cache (`type=gha`) is Linux-only and is not used here regardless.
|
||||
|
||||
### Decision: Three registry logins, all reusing existing secrets
|
||||
|
||||
The job runs `docker/login-action` three times (Docker Hub, GHCR, Quay) using the same secrets as `release_android`: `DOCKER_HUB_USERNAME` / `DOCKER_HUB_TOKEN` for Docker Hub, `github.actor` / `github.token` for GHCR, `QUAY_USERNAME` / `QUAY_ROBOT_TOKEN` for Quay. No new secret rotation is needed.
|
||||
|
||||
### Decision: No `needs:` dependency between `release_android` and `release_windows`
|
||||
|
||||
Independent jobs run in parallel. If Windows build fails, Android still publishes. The workflow run as a whole reports failure, but the Android image is live. This matches how multi-arch open-source distributions usually behave: don't hold back one platform on another's flake.
|
||||
|
||||
Alternative considered:
|
||||
|
||||
- **`release_windows: needs: release_android`** so Android validates first. Rejected: if Android fails, the tag is half-cut anyway and Windows would only matter as a postmortem; in the common case (both pass) it just adds 30–60 minutes to the wall-clock of the Windows publish.
|
||||
|
||||
### Decision: Image build target is `flutter`, not a new release-only target
|
||||
|
||||
`windows.Dockerfile` already has the `flutter` stage as its production stage and `test` as the test stage. The release job builds `--target flutter`. `windows.yml` (PR test) builds `--target test`. This split is correct and matches `android.Dockerfile`.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[Risk] Windows runner is flaky and tags ship with no Windows image.** → Mitigation: `workflow_dispatch` is the supported Windows-only recovery path. The `if: github.event_name == 'push'` guard on `release_android` makes `workflow_dispatch` re-run only `release_windows`, so recovery does not re-publish Android, re-push the Docker Hub readme, or re-attempt `gh release create` (which would fail on an already-existing release). Android recovery remains fix-forward + re-tag, consistent with the established 35-run history of `release.yml`. The release notes are tag-scoped (Android already publishes), so a delayed Windows publish is observable but not a failed release.
|
||||
- **[Risk] Quay or GHCR push fails after Docker Hub push succeeds.** → Mitigation: `docker push` per-registry is idempotent; a re-run of the `release_windows` job pushes any missing tags. Document that the job is safe to re-run.
|
||||
- **[Risk] `windows-2025` minutes cost grows by 30–60 min per tag.** → Acceptable: tag cadence is roughly monthly (`flutter-version-update` PRs land on stable bumps). Annualized cost is bounded.
|
||||
- **[Trade-off] No Docker Hub description update for the Windows image.** → Acceptable: users discover the Windows variant via the GitHub README, which already documents both. A separate Docker Hub repo (`flutter-windows`) will appear bare for now.
|
||||
- **[Trade-off] No Scout scan / SARIF upload for Windows.** → Acceptable: the Scout coverage gap on Windows base images is well-known. Code-scanning dashboard remains Android-only until Scout matures.
|
||||
|
||||
## Automated Test Strategy
|
||||
|
||||
- **Pre-merge verification (the only level that matters here):** the `release.yml` workflow itself does not run on `pull_request`; it runs on `push: tags: *` and `workflow_dispatch`. Therefore this change cannot be verified by a normal PR check. The verification path is:
|
||||
1. Land the PR with the new `release_windows` job and the `if: github.event_name == 'push'` guard on `release_android`.
|
||||
2. Use `workflow_dispatch` against an existing tag (e.g., the most recent stable Flutter tag) to trigger a one-shot run before the next stable bump. Expect `release_windows` to execute and the five Android-side jobs (`release_android`, `update_description`, `record_image`, `set_bootstrap_image`, `create_github_release`) to all be reported as `skipped`. A green workflow run is the success criterion.
|
||||
3. Confirm the three pushed images exist via `docker manifest inspect`.
|
||||
- **Post-merge ongoing verification:** every monthly tag exercises the path. The `flutter-version-update` PR pipeline (spec: `flutter-version-update`) already gates that the Android image is healthy before tagging; once `p1-fix-windows-ci-tests` is in, the Windows image is also gated by its `test_windows` PR check on the upgrade PR. So the release-time signal is "PR was green and merged → tag was cut → release builds against a verified image."
|
||||
- **No new test infrastructure**: the change is GitHub Actions YAML. The `metadata-action` and `login-action` are battle-tested in `release_android`.
|
||||
|
||||
## Observability
|
||||
|
||||
- **Failure surface**: the `release_windows` job appears as its own check on the workflow run. Failures show in the GitHub Actions UI exactly like `release_android` failures do today.
|
||||
- **Per-registry push errors**: `docker push` errors are emitted to stdout by the Docker daemon and end up in the workflow log under the push step. No additional logging is needed.
|
||||
- **Image digest visible after push**: each `docker push` prints the resulting digest. Copying that digest into the run summary is a nice-to-have but not required; `docker manifest inspect` from any host gives the same answer post-hoc.
|
||||
- **No silent failures possible**: each `docker push` is its own step (or sub-command) and propagates its exit code. The job step fails on any non-zero exit.
|
||||
- **Maintainer dashboard**: `gh run list --workflow=release.yml --limit 5` shows the most recent releases with per-job status; this is the existing observability surface and continues to work.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Land `p1-fix-windows-ci-tests` first so the Windows image is actually verified per-PR.
|
||||
2. Open a PR adding the `release_windows` job to `release.yml`. The PR's `pull_request` checks do not exercise `release.yml` (it runs on `push: tags`); land it on the strength of YAML review + pinned-action diff.
|
||||
3. After merge, manually run `release.yml` via `workflow_dispatch` against the most recent stable tag to validate the new job end-to-end *before* the next monthly upgrade PR cycle. If something is wrong, hotfix in a follow-up PR rather than waiting for a real release.
|
||||
4. The next `flutter-version-update` PR that lands and is tagged exercises the path automatically.
|
||||
5. Rollback strategy: if a regression appears (e.g., Windows registry credentials are wrong), revert the PR. Tags already pushed continue to publish only Android until the revert is itself reverted.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should the `update_description` job on Docker Hub gain a parallel `update_description_windows` step pointing at a `readme-windows.md`? *Out of scope here* — split if/when there's a Windows-specific readme worth maintaining.
|
||||
@@ -0,0 +1,31 @@
|
||||
## Why
|
||||
|
||||
`release.yml` only builds and publishes the `flutter-android` image on tag. The Windows Dockerfile, the `windows-2025` test workflow, and the `IMAGE_REPOSITORY_NAME: flutter-windows` env var in `windows.yml` all imply a `flutter-windows` image is shipped — but no release path actually pushes it. Once `p1-fix-windows-ci-tests` lands and CI verifies the image, users still cannot `docker pull <org>/flutter-windows:<flutter-version>`. This change adds a `release_windows` job to `release.yml` that mirrors `release_android` for the Windows artifact, so cutting a tag publishes both images.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add a new `release_windows` job to `.github/workflows/release.yml` that runs on `windows-2025`, builds `windows.Dockerfile` with `--target flutter`, and pushes the resulting image to Docker Hub, GitHub Container Registry, and Quay.io with the `<flutter-version>` tag (matching the existing Android tagging convention).
|
||||
- Reuse `script/setEnvironmentVariables.js` and `docker/metadata-action` exactly as `release_android` does, so the tag/label conventions stay identical across architectures.
|
||||
- Login steps reuse the existing `DOCKER_HUB_*`, `QUAY_*`, and `GHCR` credentials. No new secrets are introduced.
|
||||
- The new job runs in parallel with `release_android` (no `needs:` dependency between them) so a Windows build failure does not block Android publishing and vice versa.
|
||||
- The downstream `update_description`, `record_image`, `set_bootstrap_image`, and `create_github_release` jobs that currently `needs: release_android` are NOT changed: they remain Android-scoped because the Docker Hub description, Scout environment, bootstrap-image variable, and changelog all currently reference Android only. Generalizing them is out of scope.
|
||||
- Add `if: github.event_name == 'push'` to `release_android` so that `workflow_dispatch` exercises only `release_windows` (the four Android-side downstream jobs auto-skip via their existing `needs: release_android`). This makes `workflow_dispatch` the supported Windows-only recovery path and keeps the Android image and GitHub release untouched on dry-runs.
|
||||
- The `test_windows` PR check from `p1` SHALL be a required check before tags can be cut, but the actual gating is repository-settings-only and not in this PR's diff.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `windows-image-release`: defines what `release.yml` must do on a tag push so that a `flutter-windows:<flutter-version>` image is published to the same set of registries as the Android image.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
_None._ The existing `flutter-version-update` and `actions-version-tracking` specs are unaffected. `release_android` is unchanged.
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected files: `.github/workflows/release.yml` (one new job added; `if:` guard added to `release_android`), `.github/gx.toml` (one new per-step entry under `[actions.overrides]."docker/metadata-action"` for the new step pinned at `~5.10.0`; no new top-level actions are introduced because the action itself is already in `[actions]`), `.github/gx.lock` (regenerated by `gx tidy`).
|
||||
- Depends on: `p1-fix-windows-ci-tests` landed (so the image is verified before publishing), but does not depend on `p3-windows-version-schema` (versioning of the Windows artifact follows the existing `flutter.version` convention).
|
||||
- Operational impact: every tag push triggers an additional `windows-2025` run, which currently takes 30–60 minutes. Tag-push to first-Windows-image-published wall-clock time grows by that amount.
|
||||
- Cost: `windows-2025` runner minutes are billed; the budget impact should be reviewed before this lands.
|
||||
- Risk: a flaky Windows build will block release tags. Mitigation: `workflow_dispatch` is the supported Windows-only recovery path (re-run the workflow on the existing tag without re-publishing Android or re-creating the GitHub release). Android recovery remains the established fix-forward + re-tag pattern.
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Tag push publishes a `flutter-windows` image to all release registries
|
||||
|
||||
When a tag matching `*` is pushed to the repository, the `release_windows` job in `.github/workflows/release.yml` SHALL build `windows.Dockerfile` with `--target flutter` and `--build-arg flutter_version=<tag>`, and SHALL push the resulting image to Docker Hub, GitHub Container Registry, and Quay.io under the repository name `flutter-windows` with the tag equal to the Flutter version.
|
||||
|
||||
The experience context is the CI engineer who, on the day a new Flutter stable lands, expects to run `docker pull docker.io/<org>/flutter-windows:<version>` and find the image at the same tag they already use for `flutter-android`.
|
||||
|
||||
#### Scenario: Tag push fans out to all three registries
|
||||
|
||||
- **GIVEN** a tag `X.Y.Z` is pushed to the repository
|
||||
- **WHEN** the `release_windows` job completes successfully
|
||||
- **THEN** `docker.io/<org>/flutter-windows:X.Y.Z` exists
|
||||
- **AND** `ghcr.io/<org>/flutter-windows:X.Y.Z` exists
|
||||
- **AND** `quay.io/<org>/flutter-windows:X.Y.Z` exists
|
||||
|
||||
#### Scenario: Tag-image consistency
|
||||
|
||||
- **WHEN** any of the three published `flutter-windows:X.Y.Z` images is pulled and `flutter --version` is invoked inside it
|
||||
- **THEN** the reported Flutter version is exactly `X.Y.Z`
|
||||
|
||||
### Requirement: Windows release runs in parallel with Android release
|
||||
|
||||
The `release_windows` job SHALL NOT declare a `needs:` dependency on `release_android`, and `release_android` SHALL NOT declare a `needs:` dependency on `release_windows`. A failure in one SHALL NOT cancel the other.
|
||||
|
||||
The experience context is the maintainer cutting a release: they accept that one architecture may publish while the other fails, and prefer fixing the failed one in a follow-up tag rather than blocking both.
|
||||
|
||||
#### Scenario: Android publishes when Windows build fails
|
||||
|
||||
- **GIVEN** a tag is pushed
|
||||
- **AND** the `release_windows` job fails (e.g., transient `windows-2025` runner issue)
|
||||
- **AND** the `release_android` job succeeds
|
||||
- **WHEN** the workflow run completes
|
||||
- **THEN** Android images are published at all three registries
|
||||
- **AND** the workflow run is reported as failed (because at least one job failed)
|
||||
- **AND** the failure surface is the `release_windows` job specifically, not `release_android`
|
||||
|
||||
### Requirement: Windows release uses the same metadata conventions as Android release
|
||||
|
||||
The `release_windows` job SHALL use `docker/metadata-action` with the `images` input set to the same three registry namespaces and the `tags` input set to `type=raw,value=${{ env.FLUTTER_VERSION }}`, mirroring the Android job. The image labels (`org.opencontainers.image.*`) produced by `metadata-action` SHALL be applied to the built image (e.g., as `--label` arguments to `docker build`), so that `docker inspect` reports the same OCI label set as the Android image. `docker/build-push-action` is not a viable mechanism here because it does not support Windows containers (tracked at https://github.com/docker/build-push-action/issues/18).
|
||||
|
||||
The experience context is the operator inspecting `docker inspect <org>/flutter-windows:X.Y.Z` and `docker inspect <org>/flutter-android:X.Y.Z` and finding the same set of OCI labels (description, source, revision, version) populated with the same values.
|
||||
|
||||
#### Scenario: Labels match Android conventions
|
||||
|
||||
- **GIVEN** a successful `release_windows` run for tag `X.Y.Z`
|
||||
- **WHEN** an operator runs `docker inspect docker.io/<org>/flutter-windows:X.Y.Z` and inspects the `Labels` map
|
||||
- **THEN** the keys `org.opencontainers.image.source`, `org.opencontainers.image.revision`, `org.opencontainers.image.version`, and `org.opencontainers.image.title` are all present
|
||||
- **AND** `org.opencontainers.image.version` equals `X.Y.Z`
|
||||
- **AND** `org.opencontainers.image.revision` equals the commit SHA of the tag
|
||||
|
||||
### Requirement: Manual `workflow_dispatch` rebuild is Windows-only
|
||||
|
||||
The `release.yml` workflow SHALL continue to declare `workflow_dispatch:`. On `workflow_dispatch`, only the `release_windows` job SHALL execute; `release_android` and its downstream jobs (`update_description`, `record_image`, `set_bootstrap_image`, `create_github_release`) SHALL be skipped via an `if: github.event_name == 'push'` guard on `release_android` (the four downstream jobs auto-skip via their existing `needs: release_android`). The `FLUTTER_VERSION` env var SHALL be set from `github.ref_name`, so that a maintainer can rebuild a single tag's Windows image without re-cutting the Git tag and without re-publishing the Android image, re-pushing the Docker Hub readme, or re-attempting `gh release create` (which would fail because the release already exists).
|
||||
|
||||
The experience context is the maintainer recovering from a transient Windows runner failure: they re-run the workflow on the existing tag instead of force-pushing a new one. Android recovery, by contrast, is the established fix-forward + re-tag pattern (see `release.yml` run history) and does not need a `workflow_dispatch` path.
|
||||
|
||||
#### Scenario: Manual rebuild produces a fresh Windows image
|
||||
|
||||
- **GIVEN** a tag `X.Y.Z` exists in the repository
|
||||
- **AND** the prior `release_windows` run for that tag failed
|
||||
- **WHEN** a maintainer triggers `release.yml` via `workflow_dispatch` selecting ref `X.Y.Z`
|
||||
- **THEN** `release_windows` builds and pushes `flutter-windows:X.Y.Z` to all three registries
|
||||
- **AND** the existing Windows image digests at those tags are overwritten by the new digests
|
||||
|
||||
#### Scenario: Manual rebuild leaves the Android digest untouched
|
||||
|
||||
- **GIVEN** a tag `X.Y.Z` exists and was previously published with Android digest `D_a`
|
||||
- **WHEN** a maintainer triggers `release.yml` via `workflow_dispatch` selecting ref `X.Y.Z`
|
||||
- **THEN** `release_android` is reported as `skipped`
|
||||
- **AND** `update_description`, `record_image`, `set_bootstrap_image`, and `create_github_release` are reported as `skipped`
|
||||
- **AND** the digest at `docker.io/<org>/flutter-android:X.Y.Z` remains `D_a`
|
||||
- **AND** the run is reported as success (no failed jobs)
|
||||
@@ -0,0 +1,51 @@
|
||||
## 1. Add the `release_windows` job to `release.yml`
|
||||
|
||||
- [x] 1.1 Open `.github/workflows/release.yml` and add a new job `release_windows` after `release_android`. Set `runs-on: windows-2025`, `permissions.packages: write`, `env.IMAGE_REPOSITORY_NAME: flutter-windows`, `env.VERSION_MANIFEST: config/version.json`.
|
||||
- [x] 1.2 Add a `Checkout repository` step using the same SHA-pinned `actions/checkout` already in use elsewhere in the file.
|
||||
- [x] 1.3 Add a `Read environment variables from the version manifest` step using `actions/github-script` and `script/setEnvironmentVariables.js`, identical to `release_android`.
|
||||
- [x] 1.4 Add a `Load image metadata` step using `docker/metadata-action` with `images:` set to `${{ env.IMAGE_REPOSITORY_PATH }}`, `ghcr.io/${{ env.IMAGE_REPOSITORY_PATH }}`, `quay.io/${{ env.IMAGE_REPOSITORY_PATH }}` and `tags: type=raw,value=${{ env.FLUTTER_VERSION }}`.
|
||||
|
||||
## 2. Wire registry logins
|
||||
|
||||
- [x] 2.1 Add `Login to Docker Hub` step using `docker/login-action` with `${{ secrets.DOCKER_HUB_USERNAME }}` / `${{ secrets.DOCKER_HUB_TOKEN }}`.
|
||||
- [x] 2.2 Add `Login to GitHub Container Registry` step with `registry: ghcr.io`, `${{ github.actor }}` / `${{ github.token }}`.
|
||||
- [x] 2.3 Add `Login to Quay.io` step with `registry: quay.io`, `${{ secrets.QUAY_USERNAME }}` / `${{ secrets.QUAY_ROBOT_TOKEN }}`.
|
||||
|
||||
## 3. Build and push the Windows image
|
||||
|
||||
- [x] 3.1 Add a `Build image` step running `docker build . -f windows.Dockerfile --target flutter --build-arg flutter_version=${{ env.FLUTTER_VERSION }}` followed by `docker tag` calls that apply each metadata-action tag to the local image.
|
||||
- [x] 3.2 Add the OCI labels emitted by `metadata-action` to the build using `--label` arguments (or pipe the labels via a script step that iterates `${{ steps.metadata.outputs.labels }}`).
|
||||
- [x] 3.3 Add a `Push to registries` step that runs `docker push` for each tag in `${{ steps.metadata.outputs.tags }}` (one push per registry-prefixed tag).
|
||||
|
||||
## 4. Confirm parallelism and isolation from `release_android`
|
||||
|
||||
- [x] 4.1 Verify the new job has no `needs:` line and no `if:` line keying on `release_android` outcome — it must run in parallel.
|
||||
- [x] 4.2 Verify the existing `update_description`, `record_image`, `set_bootstrap_image`, and `create_github_release` jobs still `needs: release_android` only, not `release_windows`.
|
||||
- [x] 4.3 Add `if: github.event_name == 'push'` to `release_android` so that `workflow_dispatch` runs `release_windows` in isolation. The four Android-side downstream jobs auto-skip via their existing `needs: release_android` (GitHub Actions skips dependents when their `needs` job is skipped), so no `if:` clause is added to them.
|
||||
|
||||
## 5. Confirm gx pinning compliance
|
||||
|
||||
- [x] 5.1 Confirm every `uses:` action in the new job is already entered in `.github/gx.toml` (it should be, since they all appear in `release_android`).
|
||||
- [x] 5.2 Add a new entry to `[actions.overrides]."docker/metadata-action"` in `.github/gx.toml` for the new `release_windows` step, pinned at `~5.10.0` for parity with `release_android` (the existing `~5.7.0` entry for `windows.yml::test_windows` is unrelated and is left untouched in this change).
|
||||
- [x] 5.3 Run `gx tidy` locally; the diff should be empty after 5.2 lands. If it isn't, commit the gx-managed updates with the change.
|
||||
- [x] 5.4 Run `gx lint` locally to confirm SHA pinning is correct.
|
||||
|
||||
## 6. Pre-merge dry run
|
||||
|
||||
- [ ] 6.1 Push the branch and open a PR. The `pull_request` checks do not exercise `release.yml`, so the PR is evaluated on YAML review only.
|
||||
- [ ] 6.2 After merge, use `workflow_dispatch` to trigger `release.yml` against the most recent stable Flutter tag. The run SHALL report `release_windows` as success and `release_android`, `update_description`, `record_image`, `set_bootstrap_image`, and `create_github_release` all as `skipped`. A green workflow run is the success criterion.
|
||||
- [ ] 6.3 Confirm `release_windows` exits 0 and the three published manifests exist:
|
||||
- `docker manifest inspect docker.io/<org>/flutter-windows:<version>`
|
||||
- `docker manifest inspect ghcr.io/<org>/flutter-windows:<version>`
|
||||
- `docker manifest inspect quay.io/<org>/flutter-windows:<version>`
|
||||
- [ ] 6.4 Confirm `docker.io/<org>/flutter-android:<version>` digest is unchanged from the original tag-time publish. This is structurally guaranteed by the `if:` guard on `release_android` (task 4.3), but verify once on the first dry-run.
|
||||
|
||||
## 7. Confirm OCI labels and version match
|
||||
|
||||
- [ ] 7.1 Run `docker pull docker.io/<org>/flutter-windows:<version>` and `docker inspect` it.
|
||||
- [ ] 7.2 Confirm `Labels["org.opencontainers.image.version"]` equals `<version>` and `Labels["org.opencontainers.image.revision"]` equals the tag's commit SHA.
|
||||
- [ ] 7.3 Run the image and confirm `flutter --version` reports `<version>`.
|
||||
|
||||
## 8. Archive
|
||||
|
||||
- [ ] 8.1 After merge and successful first real (non-dispatch) release, archive this change so the `windows-image-release` spec is promoted to `openspec/specs/`.
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-05-13
|
||||
@@ -0,0 +1 @@
|
||||
passed
|
||||
@@ -0,0 +1,69 @@
|
||||
## Context
|
||||
|
||||
p2 establishes that the build job produces either a registry tag (`image_ref`) or an artifact (`image_artifact`). This change splits the existing serial `test_image` job into three jobs: `build_image`, `test_image`, `scan_image`. The latter two consume the handoff in parallel.
|
||||
|
||||
The non-trivial questions are: (a) how do consumer jobs get the bits onto their runner with minimum overhead, (b) what's the failure model when one consumer fails, and (c) how do we manage branch-protection migration without locking out merges during the transition.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- `test_image` and `scan_image` run as siblings, not in series.
|
||||
- The consumer jobs are thin: no `clean-runner-disk` needed unless the artifact path is in use (the pull does not write the full image to disk twice).
|
||||
- Branch-protection migration is documented and ordered so `main` is never unprotected and a stale required check name doesn't block merges indefinitely.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Matrix-ing CST configs (the project has one `test/android.yml`; if it ever grows, that's a separate change).
|
||||
- Caching the Scout vulnerability DB (no public knob for this per the action README).
|
||||
- Self-hosted consumers.
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1. Consumer jobs reach the image with the cheapest call each tool supports
|
||||
|
||||
**Decision**: The two consumers use different strategies because the two actions support different things:
|
||||
|
||||
- `docker/scout-action`: SHALL `docker pull` the GHCR image, re-tag it as `<owner>/flutter-android:<flutter_version>` (the Docker Hub repo path), and pass `image: local://<owner>/flutter-android:<flutter_version>`. The `registry://` prefix was the initial choice (no daemon involvement per the action README) but was rejected during implementation: Scout's `compare` command looks up the image's repo in its stream-environment records, and those records exist only for the Docker Hub repo path — passing a `registry://ghcr.io/...` ref fails with "not in stream environment:prod". The re-tag-and-`local://` trick is carried over from the previous serial implementation.
|
||||
- `container-structure-test` (via `plexsystems/container-structure-test-action`): the action invokes `container-structure-test test --image <input>` with no `--pull` flag and no driver override. The CLI's default `docker` driver inspects the local Docker daemon only. So `test_image` SHALL run an explicit `docker pull "$IMAGE_REF"` on the registry path before invoking the action. The earlier draft of this design assumed CST would stream-pull on demand; it does not.
|
||||
|
||||
**Alternatives considered**:
|
||||
|
||||
- *Replace the plexsystems action with raw `container-structure-test test --pull --image <ref>`.* One step instead of two, and drops a stale dependency (plexsystems' last release was Mar 2023; last commit Aug 2023). Rejected for now: requires a new install step (curl + chmod-and-pin), and the win is ~30 s. Worth revisiting if the action ever blocks an upgrade.
|
||||
- *Swap to CST's `--driver tar` (no daemon at all, e.g. `crane export <ref> | container-structure-test --driver tar -`).* Smallest disk footprint, but the tar driver has historical limitations around `commandTests` that rely on real process execution. Out of scope for p3.
|
||||
- *`docker save`-style artifact even for non-fork PRs.* Rejected — wastes the registry-cache work from p1.
|
||||
|
||||
**Rationale**: Match the cheapest path to each action's actual behavior, verified against current upstream sources rather than assumed.
|
||||
|
||||
### D2. Fork-PR consumers `download-artifact` + `docker load`, gated on `image_artifact != ''`
|
||||
|
||||
**Decision**: Both consumer jobs check `needs.build_image.outputs.image_artifact`. When non-empty, run a setup block: `download-artifact` → `gunzip image.tar.gz` → `docker load`. Then use the loaded image's tag (read from `metadata-action` output passed through `build_image.outputs.image_local_tag`) for the CST/Scout call.
|
||||
|
||||
**Alternatives considered**:
|
||||
|
||||
- *Skip Scout entirely for fork PRs.* Already the status quo (`build.yml:113`). Keep it for the scan job; the test job has no such restriction.
|
||||
- *Pull from a public mirror.* No public mirror exists for a not-yet-merged fork PR's image.
|
||||
|
||||
**Rationale**: The ~2 min artifact handoff cost is paid only on fork PRs and is bounded.
|
||||
|
||||
### D3. Branch protection migration
|
||||
|
||||
**Decision**: The new consumer job is named `test_image` — the same key as today's monolithic job. The check name `test_image` therefore continues to appear and continues to be required by branch-protection. The new `build_image` and `scan_image` checks are added but are NOT yet required. A follow-up admin action adds them to required status checks once the new layout has shown a few green runs.
|
||||
|
||||
**Failure semantics** during the transition:
|
||||
|
||||
- If `build_image` fails, `test_image` is skipped. GitHub treats a skipped required check as not-reported → PR cannot merge. Equivalent safety to today.
|
||||
- If `scan_image` fails but `test_image` passes, the PR can merge. This is a regression in safety relative to today (Scout currently blocks). The gap is bounded to the window between merging this change and the admin adding `scan_image` to required checks. Acceptable transient.
|
||||
|
||||
**Alternatives considered**:
|
||||
|
||||
- *Aggregator job that `needs: [build_image, scan_image]` and exits 0 under a name like `image-checks`.* Equivalent end-state, more YAML. Rejected because the name-preservation strategy already keeps `main` protected without an extra job.
|
||||
- *Block this PR until protection is updated in the same merge window.* Rejected — couples a workflow-change PR to admin availability.
|
||||
|
||||
**Rationale**: Preserving the existing check name by renaming the consumer is the smallest possible migration. The bounded scan-gap is the price; it is documented so the admin treats it as a follow-up rather than discovering it via an exploit.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **R1**: A consumer-job pull failure (transient GHCR error) fails one check while the other passes. Re-run-failed re-runs that single job, but the re-run consumes the same `pr-N` tag, which is safe.
|
||||
- **R2**: If p2 ever pushes an inconsistent `pr-N` (build succeeded but push partially failed), both consumers will fail in the same way. p2's task list requires the push to be a hard failure of the build job, which prevents this.
|
||||
- **R3**: Fork-PR `docker load` materializes 5 GB on the consumer runner. Run `clean-runner-disk` first on the fork path.
|
||||
@@ -0,0 +1,59 @@
|
||||
## Why
|
||||
|
||||
`build.yml/test_image` runs `Test image` (~4½ min, container-structure-test) and `Scan with Docker Scout` (~9 min, vulnerability scan) **back-to-back in the same job** even though neither depends on the other's output. Each consumes the just-built image independently. The serial layout makes Scout the long pole of every PR run, holding the job at ~20 min wall-clock when ~12 min is achievable.
|
||||
|
||||
The existing `build.yml:108` TODO explicitly notes this:
|
||||
|
||||
> `# TODO: Parallelize testing and vulnerability scanning`
|
||||
|
||||
`docker/scout-action` accepts a remote image reference (`registry://` prefix per the action README, no daemon required), and `container-structure-test` runs against any image present in the local Docker daemon (the `plexsystems/container-structure-test-action` does not pull on its own, so consumers on the registry path SHALL `docker pull` before invoking it). With the handoff established by p2, the two consumers can run as sibling jobs that each materialize the handoff image independently.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Rename** `test_image` job → `build_image`. Its responsibility is now: build the image, push the handoff (per p2), and stop. Remove the `Test image` and `Scan with Docker Scout` steps from this job.
|
||||
- **New job** `test_image` (`needs: build_image`): materializes the handoff into the local Docker daemon (registry-path: `docker pull <image_ref>`; artifact-path: `download-artifact` + `docker load`), runs container-structure-test against the loaded image.
|
||||
- **New job** `scan_image` (`needs: build_image`): runs `docker/scout-action` against the handoff. Registry-path `docker pull`s the GHCR image, re-tags it to the Docker Hub repo path (`<owner>/flutter-android:<flutter_version>`), and passes `image: local://<repo-path>` — Scout's `compare` requires the Docker Hub repo path to look up stream-environment records (`registry://ghcr.io/...` fails with "not in stream environment:prod"). Artifact-path uses `image: local://<image_local_tag>` after `download-artifact` + `docker load`. Gated `if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository` (Scout needs the Docker Hub org secret + PR-comment write, neither available to fork PRs — matches the existing gate at `build.yml:155`).
|
||||
- Both consumer jobs run on `ubuntu-24.04` with a thin checkout + login + pull-or-load + the existing validation step. No `setup-buildx` (neither consumer builds; buildx adds ~10 s per job for no benefit). No `clean-runner-disk` on the registry path (a 5 GB pull fits in the runner's ~14 GB free); the artifact path runs `clean-runner-disk` first because the 2 GB tarball + 5 GB extracted image is tight.
|
||||
- Pin `docker/scout-action` to **v1.20.4** (current as of Apr 2026; today's pin is v1.18.2) while the surrounding job is being rewritten — incidental cleanup, not the goal of this change.
|
||||
- `build_image` keeps `clean-runner-disk` (the build still needs the headroom).
|
||||
- Move the `permissions.security-events: write` from `build_image` to `scan_image` (Scout writes SARIF), and `permissions.pull-requests: write` likewise.
|
||||
|
||||
```
|
||||
BEFORE (one job, serial) AFTER (three jobs, parallel)
|
||||
|
||||
┌──────────────────────────┐ ┌──────────────────────┐
|
||||
│ test_image ~20m │ │ build_image ~6m │
|
||||
│ clean ........ 3m │ │ clean ........ 3m │
|
||||
│ build ........ 2½m │ │ build+push .... 3m │
|
||||
│ CST .......... 4½m │ └──────┬───────────────┘
|
||||
│ Scout ........ 9m │ │
|
||||
└──────────────────────────┘ ┌────┴─────┐
|
||||
▼ ▼
|
||||
┌──────────┐ ┌──────────────┐
|
||||
│test_image│ │ scan_image │
|
||||
│ pull+CST │ │ Scout(reg://)│
|
||||
│ ~5m │ │ ~9m │
|
||||
└──────────┘ └──────────────┘
|
||||
|
||||
Total wall: 6m + max(5,9) ≈ 15m
|
||||
(vs. 20m → saves ~5m; with p1 ~7m)
|
||||
```
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `ci-parallel-image-validation`: defines the contract that container-structure testing and vulnerability scanning of the Flutter Docker image SHALL run as siblings — not as serial steps in the same job — and the dependency rules each consumer SHALL satisfy.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
_None._ p2's `ci-image-handoff` is consumed but not modified.
|
||||
|
||||
## Impact
|
||||
|
||||
- **Affected files**: `.github/workflows/build.yml` (substantial reorganization of the `test_image` job into three jobs).
|
||||
- **Behavioral change**: PR check page shows three checks (`build_image`, `test_image`, `scan_image`) instead of one (`test_image`). Required-status-check protection rules on `main` SHALL be updated to require the new names — branch protection is the migration blocker, called out explicitly in tasks.
|
||||
- **Wall-clock win**: ~5 min saved per PR run on non-fork PRs (post-p1: ~7 min). Fork PRs save the same, paid via ~2 min of artifact upload/download overhead — net ~3 min saved.
|
||||
- **Risk**: a fork-PR consumer job loading a 2 GB artifact and then `docker load`-ing it adds disk pressure on the consumer runners. Mitigation: consumer jobs run `clean-runner-disk` selectively when `image_artifact` is non-empty (artifact path), skip it when `image_ref` is set (registry path uses streaming pull, no save-file on disk).
|
||||
- **Depends on**: p2 (handoff). Cannot land without it.
|
||||
- **Out of scope**: changing what CST or Scout do, splitting CST configs by Dockerfile stage, Windows image parallelization.
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Test and scan run as sibling jobs, not as serial steps
|
||||
|
||||
Container-structure-test and Docker Scout SHALL run as two separate GitHub Actions jobs (`test_image` and `scan_image`), each with `needs: build_image`, in the same workflow. They SHALL NOT run as sequential steps inside the same job. The two jobs SHALL be schedulable in parallel by the runner — no `needs` relationship between them, no shared sentinel files, no implicit ordering.
|
||||
|
||||
The experience context is the maintainer watching the PR check page — they should see two checks start within seconds of `build_image` turning green, run in parallel, and the slower one (Scout) define the wall-clock rather than the sum.
|
||||
|
||||
#### Scenario: Test job runs in parallel with scan job
|
||||
|
||||
- **GIVEN** a PR run where `build_image` has just completed successfully
|
||||
- **WHEN** the runner picks up the downstream jobs
|
||||
- **THEN** `test_image` and `scan_image` both start within 30 seconds of `build_image` completion
|
||||
- **AND** their start times are within 30 seconds of each other (no implicit serialization)
|
||||
|
||||
#### Scenario: Scout scan runs in parallel with CST
|
||||
|
||||
- **GIVEN** a PR run with both consumer jobs running
|
||||
- **WHEN** the overall wall-clock is measured
|
||||
- **THEN** total time from `build_image` start to last consumer complete is approximately `build_image_duration + max(test_image_duration, scan_image_duration)` (not the sum)
|
||||
|
||||
### Requirement: Consumer jobs do not rebuild the image
|
||||
|
||||
`test_image` and `scan_image` SHALL consume the image via the handoff produced by `build_image` (per the `ci-image-handoff` capability). Neither job SHALL invoke `docker build`, `docker/build-push-action`, or any other Dockerfile build action. They SHALL only `pull` (registry path) or `download-artifact` + `docker load` (fork-PR path).
|
||||
|
||||
The experience context is the maintainer auditing CI cost — they expect each Dockerfile-touch PR to materialize the image bits exactly once, not three times.
|
||||
|
||||
#### Scenario: Consumer pulls registry image without rebuilding
|
||||
|
||||
- **GIVEN** a non-fork PR run
|
||||
- **WHEN** `test_image` or `scan_image` runs
|
||||
- **THEN** the job log shows a pull (or streaming pull by the action) of `ghcr.io/<owner>/flutter-android:pr-<N>`
|
||||
- **AND** the job log does not contain `docker build` or `FROM debian:`
|
||||
|
||||
#### Scenario: Fork PR consumer loads the artifact without rebuilding
|
||||
|
||||
- **GIVEN** a fork PR run
|
||||
- **WHEN** `test_image` or `scan_image` runs
|
||||
- **THEN** the job downloads `image-<run_id>`, runs `docker load`, and proceeds
|
||||
- **AND** the job log does not contain `docker build`
|
||||
|
||||
### Requirement: Scan job preserves the existing fork-PR gate
|
||||
|
||||
`scan_image` SHALL preserve the gate that today restricts `docker/scout-action` to non-fork PRs (Scout requires the Docker Hub org secret and writes a PR comment, neither available to fork PRs). The gate SHALL be applied at the job level (`if:` on the job), not as a per-step skip — fork-PR scan jobs SHALL be entirely skipped, not show as a no-op success step.
|
||||
|
||||
The experience context is the community contributor opening a fork PR — they see `build_image` and `test_image` run, and `scan_image` simply not appear (skipped), rather than appearing as a confusingly-empty job.
|
||||
|
||||
#### Scenario: Fork PR skips scan_image entirely
|
||||
|
||||
- **GIVEN** a `pull_request` event with `github.event.pull_request.head.repo.full_name != github.repository`
|
||||
- **WHEN** the workflow runs
|
||||
- **THEN** `scan_image` does not appear in the run's job list (job-level `if:` evaluates to false)
|
||||
- **AND** the run still completes successfully when `build_image` and `test_image` succeed
|
||||
|
||||
### Requirement: Renamed consumer preserves the existing required-check name
|
||||
|
||||
The CST consumer job's key SHALL be `test_image` — the same name as today's monolithic job — so the GitHub Actions check name `test_image` continues to appear and continues to satisfy branch-protection rules that require it. The previously-monolithic job SHALL be the one that is renamed to `build_image`, not the other way around.
|
||||
|
||||
The experience context is the maintainer merging this change — they do not need privileged admin access to update branch-protection rules to merge this PR, because the `test_image` check name is unchanged. Admin work (adding `build_image` and `scan_image` to required checks) happens as a follow-up after the new layout has proven stable.
|
||||
|
||||
#### Scenario: Renamed consumer satisfies the existing required check
|
||||
|
||||
- **GIVEN** branch-protection requires the check name `test_image`
|
||||
- **WHEN** the workflow runs after this change merges
|
||||
- **THEN** a check named `test_image` is reported (produced by the CST consumer job)
|
||||
- **AND** when `build_image` succeeds and the consumer succeeds, the `test_image` check is green
|
||||
- **AND** the PR can merge without admin intervention
|
||||
|
||||
#### Scenario: build_image failure correctly blocks merges
|
||||
|
||||
- **GIVEN** `build_image` fails
|
||||
- **WHEN** `test_image` runs
|
||||
- **THEN** `test_image` is skipped (its `needs:` failed)
|
||||
- **AND** the required check `test_image` is not reported
|
||||
- **AND** branch-protection treats the check as not-satisfied, blocking the merge
|
||||
@@ -0,0 +1,44 @@
|
||||
## 1. Rename `test_image` → `build_image` and strip the validation steps
|
||||
|
||||
- [x] 1.1 Rename the `test_image` job key in `build.yml` to `build_image`. Update any references in the workflow.
|
||||
- [x] 1.2 Remove the `Test image` step (`plexsystems/container-structure-test-action`) from `build_image`.
|
||||
- [x] 1.3 Remove the `Scan with Docker Scout` step from `build_image`.
|
||||
- [x] 1.4 Keep `clean-runner-disk`, buildx setup, logins, metadata, and the build+push step (per p2).
|
||||
- [x] 1.5 Drop `permissions.security-events: write` and `permissions.pull-requests: write` from `build_image` — they belong to `scan_image` now.
|
||||
|
||||
## 2. Add the new `test_image` consumer job
|
||||
|
||||
- [x] 2.1 Add a job `test_image` with `needs: build_image`, `runs-on: ubuntu-24.04`, `permissions.contents: read` and `permissions.packages: read`. Do NOT add `setup-buildx-action` — the job does not build.
|
||||
- [x] 2.2 Checkout the repo (CST needs `test/android.yml`).
|
||||
- [x] 2.3 Branch on `needs.build_image.outputs.image_artifact`:
|
||||
- Non-empty (fork PR): run `clean-runner-disk`, `download-artifact`, `gunzip`, `docker load`, then invoke CST against `needs.build_image.outputs.image_local_tag`.
|
||||
- Empty (non-fork): GHCR login (read) + `docker pull "$IMAGE_REF"` (where `IMAGE_REF=needs.build_image.outputs.image_ref`), then invoke CST against `IMAGE_REF`. The pull is required because `plexsystems/container-structure-test-action` does not pass `--pull` and the underlying CLI's `docker` driver only inspects the local daemon — passing a registry ref without a prior pull fails with "image not found".
|
||||
- [x] 2.4 Use `plexsystems/container-structure-test-action` with `config: test/android.yml` and `image: <ref-or-loaded-tag>` — satisfies spec scenario "Test job runs in parallel with scan job".
|
||||
|
||||
## 3. Add the new `scan_image` consumer job
|
||||
|
||||
- [x] 3.1 Add a job `scan_image` with `needs: build_image`, `runs-on: ubuntu-24.04`, `permissions.packages: read`, `permissions.pull-requests: write`, `permissions.security-events: write`. Do NOT add `setup-buildx-action` — the job does not build, and Scout reads from the registry directly.
|
||||
- [x] 3.2 Gate the entire job: `if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository` (Scout's existing fork gate).
|
||||
- [x] 3.3 Branch on `image_artifact`:
|
||||
- Non-empty (artifact path): `clean-runner-disk`, `download-artifact`, `gunzip`, `docker load`, then pass `image: local://<image_local_tag>` to `docker/scout-action`.
|
||||
- Empty (registry path): GHCR login (read), then pass `image: registry://<image_ref>` to `docker/scout-action` — no `docker pull` needed (the `registry://` prefix tells Scout to bypass the local image store).
|
||||
- [x] 3.4 Preserve all current Scout inputs: `command: compare, recommendations`, `github-token`, `only-fixed: true`, `organization: ${{ secrets.DOCKER_HUB_USERNAME }}`, `to-env: prod`.
|
||||
- [x] 3.5 Bump the `docker/scout-action` pin from v1.18.2 → **v1.20.4** (current as of Apr 2026) while rewriting the step. Incidental cleanup; mention it in the PR body so reviewers can read the release notes (https://github.com/docker/scout-action/releases) and grant approval consciously.
|
||||
- [x] 3.6 Remove the inline TODO `# TODO: Parallelize testing and vulnerability scanning` — this change resolves it. Satisfies spec scenario "Scout scan runs in parallel with CST".
|
||||
|
||||
## 4. Branch-protection migration
|
||||
|
||||
- [x] 4.1 Verify the new consumer job key is exactly `test_image` (same as today's monolithic job). The existing required-check named `test_image` continues to be produced — satisfies spec scenario "Renamed consumer preserves the existing required-check name".
|
||||
- [x] 4.2 Inspect current required checks: `gh api repos/gmeligio/flutter-docker-image/branches/main/protection` → 404 "Branch not protected". No required checks are configured on `main` — no migration needed.
|
||||
- [ ] 4.3 After this PR merges and produces 3 successful runs on `main`, add `build_image`, `test_image`, and `scan_image` as required status checks. Since no protection exists today, this sets up protection from scratch rather than migrating existing rules.
|
||||
|
||||
## 5. Verify on a real PR before merge
|
||||
|
||||
- [x] 5.1 PR-A (non-fork): confirm `build_image`, `test_image`, `scan_image` all run, the two consumers start within ~10 s of `build_image` completing, and overall wall-clock is ≤ 15 min (target: ~12-13 min once p1 is in).
|
||||
- [x] 5.2 PR-A (fork): confirm the artifact path works end-to-end: `build_image` uploads `image-<run_id>`, `test_image` and `scan_image` (scan only if scan would run for forks — it does not today) download and `docker load` and validate.
|
||||
- [x] 5.3 Confirm that if `test_image` fails but `scan_image` passes (or vice versa), the PR shows the partial failure correctly and re-run-failed reruns only the failed job.
|
||||
|
||||
## 6. Post-merge closure check
|
||||
|
||||
- [x] 6.1 After 10 post-merge runs, query the median wall-clock of the longest job in `build.yml` and confirm it is ≤ 15 min (down from ~20 min). If above target, investigate which step regressed (likely the `docker pull` on consumers — preferable to fix the cache rather than re-merge).
|
||||
- [x] 6.2 Sweep open PRs after merge: any in-flight PR built on the old job layout will show the old `test_image` check as missing on rebase. Document the rebase recipe in the merge commit message.
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-05-23
|
||||
@@ -0,0 +1 @@
|
||||
passed
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user