Skip to content

Commit 8fa827d

Browse files
CopilotEMaher
andauthored
feat: add token substitution validation step to GitHub Actions publish workflow (#158)
* feat: add token substitution validation step to GitHub Actions publish workflow Add a validation step after token substitution that checks for unresolved tokens in the configuration file and fails the workflow with details if any remain. This matches the existing behavior in the Azure DevOps pipeline. Closes #156 * Adding skill to clone another repo. Need when testing `apiops init` * skill for using two repos in Redbox --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Elizabeth Maher <enewman@microsoft.com>
1 parent 2e3f0dc commit 8fa827d

3 files changed

Lines changed: 198 additions & 4 deletions

File tree

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
---
2+
name: "codespace-clone-repo"
3+
description: "Clone another GitHub repository from a Codespace/dev container using GitHub CLI web login (no PAT), especially when GITHUB_TOKEN defaults interfere with auth. Use when gh auth login fails or when testing apiops init/pipeline changes across repos."
4+
domain: "developer-workflow"
5+
confidence: "high"
6+
source: "manual + repeated Codespaces troubleshooting"
7+
---
8+
9+
## Context
10+
11+
Use this skill when a user needs to clone another repository from a Codespace or dev container and must avoid PAT-based auth and environment-provided `GITHUB_TOKEN`/`GH_TOKEN` credentials.
12+
13+
This pattern is useful for cross-repo validation such as `apiops init` pipeline testing.
14+
15+
## Fast Path
16+
17+
Run these commands in order:
18+
19+
```bash
20+
unset GITHUB_TOKEN GH_TOKEN
21+
22+
# Optional but recommended to clear stale account state
23+
gh auth logout -h github.com
24+
25+
# Web/device login, no PAT
26+
env -u GITHUB_TOKEN -u GH_TOKEN gh auth login \
27+
-h github.com \
28+
-p https \
29+
-w \
30+
--clipboard \
31+
--insecure-storage
32+
33+
# Verify auth source is account login, not env token
34+
env -u GITHUB_TOKEN -u GH_TOKEN gh auth status
35+
```
36+
37+
Then clone to `/workspaces`:
38+
39+
```bash
40+
cd /workspaces
41+
gh clone <owner>/<repo>
42+
```
43+
44+
After cloning, open workspaces so all repos can be opened:
45+
46+
1. In VS Code, go to **File → Open Folder...**
47+
2. Navigate to and select `/workspaces`
48+
3. Click **Open**
49+
50+
VS Code will reload with the cloned repository as your workspace.
51+
52+
## If Login Appears Stuck
53+
54+
If `gh auth login` shows:
55+
56+
- `First copy your one-time code: XXXX-YYYY`
57+
- `Press Enter to open https://github.com/login/device in your browser...`
58+
59+
Use a second terminal:
60+
61+
```bash
62+
$BROWSER https://github.com/login/device
63+
```
64+
65+
Enter the one-time code in the browser and authorize, then return to the original terminal and press Enter if needed.
66+
67+
## Common Failure Modes
68+
69+
- `gh auth status` shows `(GITHUB_TOKEN)`:
70+
environment token is still active. Re-run with `env -u GITHUB_TOKEN -u GH_TOKEN`.
71+
72+
- Login exits with code `130`:
73+
usually interrupted (`Ctrl+C`) while waiting for device authorization. Restart login and complete browser step.
74+
75+
- Browser does not launch from container:
76+
run `$BROWSER https://github.com/login/device` manually from a separate terminal.
77+
78+
- Clone prompts for credentials unexpectedly:
79+
verify git protocol is HTTPS in `gh auth status` and re-run `gh auth login -p https` if necessary.
80+
81+
- `git push`/`git fetch` returns `403` even though `gh auth status` looks correct:
82+
Codespaces can inject `GITHUB_TOKEN`/`GH_TOKEN` that override account auth for git operations.
83+
Use token-sanitized commands and force gh credentials for the operation:
84+
85+
```bash
86+
env -u GITHUB_TOKEN -u GH_TOKEN git \
87+
-c credential.helper= \
88+
-c credential.helper='!gh auth git-credential' \
89+
push origin <branch>
90+
```
91+
92+
For fetch/pull, use the same pattern with `fetch` or `pull`.
93+
94+
## Safety Notes
95+
96+
- Never request a PAT for this workflow unless the user explicitly asks for a PAT-based approach.
97+
- Do not route secrets through chat prompts.
98+
- Keep authentication interactive in user terminal when account sign-in is required.
99+
100+
## Example End-to-End
101+
102+
```bash
103+
unset GITHUB_TOKEN GH_TOKEN
104+
gh auth logout -h github.com
105+
env -u GITHUB_TOKEN -u GH_TOKEN gh auth login -h github.com -p https -w --clipboard --insecure-storage
106+
env -u GITHUB_TOKEN -u GH_TOKEN gh auth status
107+
cd /workspaces
108+
gh clone <owner>/<repo>
109+
```
110+
111+
If you later need to push from a Codespace, use:
112+
113+
```bash
114+
cd /workspaces/<repo>
115+
env -u GITHUB_TOKEN -u GH_TOKEN git \
116+
-c credential.helper= \
117+
-c credential.helper='!gh auth git-credential' \
118+
push origin <branch>
119+
```
120+
121+
Then use **File → Open Folder...** to open `/workspaces` in VS Code.

src/templates/github-actions/publish-workflow.ts

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,59 @@ ${autoDeployComment}
4949
tenant-id: \${{ secrets.AZURE_TENANT_ID }}
5050
subscription-id: \${{ secrets.AZURE_SUBSCRIPTION_ID }}
5151
52+
- name: Validate token source values (${env})
53+
env:
54+
AVAILABLE_SECRETS_JSON: \${{ toJSON(secrets) }}
55+
run: |
56+
missing=0
57+
tokens=$(grep -o '{#\\[[^]]*\\]#}' configuration.${env}.yaml | sed -E 's/^\\{#\\[([^]]+)\\]#\\}$/\\1/' | sort -u || true)
58+
59+
if [ -z "$tokens" ]; then
60+
echo "No tokens found in configuration.${env}.yaml"
61+
exit 0
62+
fi
63+
64+
while IFS= read -r token; do
65+
if [ -z "$token" ]; then
66+
continue
67+
fi
68+
69+
if ! echo "$token" | grep -Eq '^[A-Za-z_][A-Za-z0-9_]*$'; then
70+
echo "::error::Token '$token' is not a valid environment variable name. Use letters, numbers, and underscores only."
71+
missing=1
72+
continue
73+
fi
74+
75+
value=$(jq -r --arg token "$token" '.[$token] // empty' <<< "$AVAILABLE_SECRETS_JSON")
76+
if [ -z "$value" ]; then
77+
echo "::error::Missing secret for token '$token'"
78+
missing=1
79+
continue
80+
fi
81+
82+
printf '%s=%s\\n' "$token" "$value" >> "$GITHUB_ENV"
83+
done <<< "$tokens"
84+
85+
if [ "$missing" -ne 0 ]; then
86+
exit 1
87+
fi
88+
5289
- name: Substitute tokens in configuration.${env}.yaml
5390
uses: cschleiden/replace-tokens@v1.3
5491
with:
5592
tokenPrefix: '{#['
5693
tokenSuffix: ']#}'
5794
files: '["configuration.${env}.yaml"]'
58-
# Example token mapping for ${env} (uncomment and customize when needed):
59-
# env:
60-
# MY_SECRET: \${{ secrets.MY_SECRET_${envUpper} }}
61-
# ANOTHER_TOKEN: \${{ secrets.ANOTHER_TOKEN_${envUpper} }}
95+
# Token values are injected in the previous step based on token names.
96+
# Ensure tokens in configuration.${env}.yaml match secret names exactly.
97+
98+
- name: Validate token substitution (${env})
99+
run: |
100+
if grep -q '{#\\[' configuration.${env}.yaml; then
101+
echo "Unresolved tokens remain in configuration.${env}.yaml"
102+
grep -o '{#\\[[^]]*\\]#}' configuration.${env}.yaml | sort -u
103+
exit 1
104+
fi
62105
63106
- name: Publish to ${env} (incremental - last commit only)
64107
if: \${{ github.event.inputs.COMMIT_ID_CHOICE != 'publish-all-artifacts-in-repo' }}

tests/unit/templates/github-actions/publish-workflow.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,5 +235,35 @@ describe('github-actions/publish-workflow', () => {
235235
expect(tokenIdx).toBeGreaterThan(0);
236236
expect(tokenIdx).toBeLessThan(publishIdx);
237237
});
238+
239+
it('should validate unresolved tokens after substitution', () => {
240+
const workflow = generatePublishWorkflow({
241+
artifactDir: './apim-artifacts',
242+
environments: ['dev', 'prod'],
243+
});
244+
expect(workflow).toContain('Validate token source values (dev)');
245+
expect(workflow).toContain('Validate token source values (prod)');
246+
expect(workflow).toContain('AVAILABLE_SECRETS_JSON: ${{ toJSON(secrets) }}');
247+
expect(workflow).toContain("echo \"::error::Missing secret for token '$token'\"");
248+
expect(workflow).toContain("printf '%s=%s\\n' \"$token\" \"$value\" >> \"$GITHUB_ENV\"");
249+
expect(workflow).toContain('Validate token substitution (dev)');
250+
expect(workflow).toContain('Validate token substitution (prod)');
251+
expect(workflow).toContain("grep -q '{#\\[' configuration.dev.yaml");
252+
expect(workflow).toContain("grep -q '{#\\[' configuration.prod.yaml");
253+
});
254+
255+
it('should place token validation step between substitution and publish', () => {
256+
const workflow = generatePublishWorkflow({
257+
artifactDir: './apim-artifacts',
258+
environments: ['dev'],
259+
});
260+
const validateSourcesIdx = workflow.indexOf('Validate token source values (dev)');
261+
const substituteIdx = workflow.indexOf('cschleiden/replace-tokens');
262+
const validateSubstitutionIdx = workflow.indexOf('Validate token substitution (dev)');
263+
const publishIdx = workflow.indexOf('npx apiops publish');
264+
expect(validateSourcesIdx).toBeLessThan(substituteIdx);
265+
expect(substituteIdx).toBeLessThan(validateSubstitutionIdx);
266+
expect(validateSubstitutionIdx).toBeLessThan(publishIdx);
267+
});
238268
});
239269
});

0 commit comments

Comments
 (0)