Googleスプレッドシートの1行から、GASでJira Cloudの課題を作成する練習例です。まず通信しない下見を行い、確認した1行だけ送ります。10行までの表を扱いますが、一度に作成するのは1件です。
旧版は作成後の処理やシートへの記録に失敗すると重複作成につながる構成でした。この例では送信前に照合IDと送信中の状態を記録し、結果不明の行を自動再送しません。Jiraとシートは一括で確定できないため、重複を完全に防ぐ保証ではありません。
Jira側の設定とAPIトークンを確認する
自分が管理できる練習用プロジェクトと架空の課題名を使います。利用アカウントにプロジェクト閲覧と課題作成の権限が必要です。プロジェクトIDと、そのプロジェクトで使える通常の課題タイプIDを管理者または作成メタデータで確認してください。サブタスクや追加の必須項目があるタイプには、このままでは対応しません。
認証方法はBasic認証の公式説明、トークンの作成・期限・失効はAPIトークンの管理を確認します。スコープ付きトークンではJiraの課題作成APIに必要なスコープとcloudIdを確認し、API接続先にapi.atlassian.com/ex/jira/{cloudId}を使います。スコープなしの既存トークンを使う場合の接続先は自分のサイトのatlassian.netです。両者を混同しないでください。
トークンには期限があります。作成画面で期限を確認し、必要な期間に限定します。トークンを本文・セル・ログ・公開コードへ貼り付けず、管理できる非共有のApps Scriptプロジェクトのスクリプトプロパティに保存します。プロパティはプロジェクト編集者から隔離された保管庫ではないため、編集者の範囲も確認してください。
| プロパティ名 | 設定する値 |
|---|---|
| JIRA_API_BASE | スコープ付き:https://api.atlassian.com/ex/jira/{cloudId} / スコープなし:https://自分のサイト.atlassian.net |
| JIRA_EMAIL | トークンを作成したアカウントのメールアドレス |
| JIRA_API_TOKEN | APIトークン。値をログへ出さない |
| JIRA_PROJECT_ID | 練習用プロジェクトの数値ID |
| JIRA_ISSUE_TYPE_ID | そのプロジェクトで作成可能な通常の課題タイプの数値ID |
| SPREADSHEET_ID | 練習用シートURLの/d/と/editの間のID |
練習用シートと下見の準備
新しいスプレッドシートに「Jira練習」というシートを作り、A1:E1に次の見出しを入力します。旧版の9列構成とは異なります。タスク番号と照合IDは空欄にし、送る1行の発行状態だけを「準備」にします。数式・個人情報・添付画像を入れず、実行中の共同編集や並べ替えを止めます。
| 発行状態 | タスク番号 | タスク名 | 説明 | 照合ID |
|---|---|---|---|---|
| 準備 | 課題作成の練習 | 架空のデータで接続を確認します。 |
非共有のApps Scriptプロジェクトへ次のコードを保存し、プロジェクト設定で上記プロパティを登録します。JIRA_DRY_RUN=trueのままpublishJiraTasksFromSheetを手動実行し、ログの対象行・プロジェクトID・課題タイプIDを確認してください。下見はJiraへの通信もシートへの書き込みも行わず、認証やJira側の必須項目までは検証しません。
const JIRA_DRY_RUN = true;
const JIRA_SHEET_NAME = 'Jira練習';
function jiraConfig_() {
const p = PropertiesService.getScriptProperties();
const read = k => {const v=p.getProperty(k);if(!v)throw new Error(k+'が未設定です');return v;};
const api = read('JIRA_API_BASE').replace(/\/$/,'');
if (!/^https:\/\/[a-z0-9][a-z0-9-]*\.atlassian\.net$/.test(api) &&
!/^https:\/\/api\.atlassian\.com\/ex\/jira\/[a-f0-9-]{36}$/i.test(api)) throw new Error('API接続先を確認してください');
const project = read('JIRA_PROJECT_ID'), type = read('JIRA_ISSUE_TYPE_ID');
if (!/^\d+$/.test(project) || !/^\d+$/.test(type)) throw new Error('プロジェクトと課題タイプはIDを設定してください');
return {api,project,type,sheetId:read('SPREADSHEET_ID'),email:read('JIRA_EMAIL'),token:read('JIRA_API_TOKEN')};
}
function publishJiraTasksFromSheet() {
const lock=LockService.getScriptLock();
if(!lock.tryLock(1000))throw new Error('別の実行が進行中です');
try {
const c=jiraConfig_();
const sheet=SpreadsheetApp.openById(c.sheetId).getSheetByName(JIRA_SHEET_NAME);
if(!sheet)throw new Error('Jira練習シートがありません');
const last=sheet.getLastRow();
if(last<2 || last>11)throw new Error('見出しを除き1〜10行にしてください');
const range=sheet.getRange(1,1,last,5), rows=range.getDisplayValues();
if(range.getFormulas().some(r=>r.some(Boolean)))throw new Error('A:E列には数式を使わないでください');
const heads=['発行状態','タスク番号','タスク名','説明','照合ID'];
if(heads.some((v,i)=>rows[0][i]!==v))throw new Error('A1:E1の見出しを確認してください');
if(rows.slice(1).some(r=>r[0] && !['準備','済'].includes(r[0])))throw new Error('送信中・結果不明の行を先にJiraと照合してください');
const targets=rows.slice(1).map((r,i)=>({r,row:i+2})).filter(x=>x.r[0]==='準備');
if(targets.length!==1)throw new Error('準備にする行は1件だけにしてください');
const {r,row}=targets[0];
if(r[1] || r[4])throw new Error('タスク番号・照合IDがある行は再送しません');
if(!r[2].trim() || r[2].length>200 || r[3].length>4000)throw new Error('タスク名1〜200文字、説明4000文字以内にしてください');
if(JIRA_DRY_RUN){console.log('未送信:対象行='+row+'、projectId='+c.project+'、typeId='+c.type);return;}
const id='sheet-'+Utilities.getUuid();
const text=r[3]+'\n照合ID: '+id;
const fields={project:{id:c.project},issuetype:{id:c.type},summary:r[2],description:{type:'doc',version:1,content:text.split(/\r?\n/).map(t=>({type:'paragraph',content:t?[{type:'text',text:t}]:[]}))}};
// 書き込み前にも入力を比較。共同編集や並べ替えは止めて実行する。
if(JSON.stringify(sheet.getRange(row,1,1,5).getDisplayValues()[0])!==JSON.stringify(r))throw new Error('入力が変更されました');
sheet.getRange(row,5).setValue(id);
sheet.getRange(row,1).setValue('送信中');
SpreadsheetApp.flush();
let key='';
try {
const res=UrlFetchApp.fetch(c.api+'/rest/api/3/issue',{
method:'post',contentType:'application/json',
headers:{Authorization:'Basic '+Utilities.base64Encode(c.email+':'+c.token),Accept:'application/json'},
payload:JSON.stringify({fields}),muteHttpExceptions:true,followRedirects:false
});
if(res.getResponseCode()!==201)throw new Error('HTTP '+res.getResponseCode());
key=JSON.parse(res.getContentText()).key;
if(typeof key!=='string' || !/^[A-Z][A-Z0-9_]*-\d+$/.test(key))throw new Error('課題キーを確認できません');
console.log('作成応答:'+key+'、照合ID='+id);
sheet.getRange(row,2).setValue(key);
SpreadsheetApp.flush();
sheet.getRange(row,1).setValue('済');
SpreadsheetApp.flush();
} catch(error) {
try {sheet.getRange(row,1).setValue('結果不明');SpreadsheetApp.flush();}catch(ignored){}
throw new Error('再送しないでください。行='+row+'、照合ID='+id+'。Jiraとシートを照合してください。');
}
} finally {lock.releaseLock();}
}
1件を送信して結果を照合する
作成先・入力行・権限を確認したらJIRA_DRY_RUN=falseにして、同じ関数を1回実行します。Jiraへの作成リクエストは1回だけです。成功応答の課題キーをB列へ保存してからA列を「済」にします。Jiraを開いて件名・説明・作成件数と説明末尾の照合IDを確認し、終わったらtrueへ戻してください。次の課題は別の行を準備にして、同じ手順で1件ずつ確認します。
説明はCreate issueの公式仕様に従ってADF形式の段落にします。担当者・報告者・優先度・初期ステータスはJira側の設定に従い、このコードから指定しません。親課題の指定、ステータス遷移、画像添付・説明内画像埋め込みも対象外です。旧版の表示名による担当者検索や画像のリダイレクトURLからIDを抽出する処理を、確認済み機能として使わないでください。
結果不明・失敗時は再送する前に確認する
「送信中」「結果不明」のままの場合、Jiraで作成済みでもシートにキーを記録できなかった可能性があります。A列やE列を消して再送せず、実行ログの課題キー、Jiraの最近作成した課題、説明の照合IDを照合します。作成済みなら正しいキーをB列に記録してA列を「済」にします。Jira側で未作成を確認できない間は再送しません。
認証切れ、権限不足、必須項目不足、送信制限でも自動再試行しません。エラー本文や認証ヘッダーはログへ出しません。管理者と設定を確認し、未作成と判断できた行だけ、原因を直した後に手動で再準備します。別のプロジェクトからの実行や人の同時編集はスクリプトロックの対象外です。
この練習はトリガーを作りません。停止時は手動実行をやめ、dryRunをtrueへ戻します。不要になったトークンは失効させ、プロパティから除去します。シートの状態を戻してもJiraの課題は消えません。作成済み課題の削除や整理は、Jira上の内容と運用ルールを確認して別途行ってください。
検証範囲と旧版の参考資料
今回確認したのは、ローカル模擬環境での未送信・対象行検証・作成成功・通信失敗・シート記録失敗・再送停止です。Jira Cloudへの実作成、トークンの認証、Googleの認可、実画面、組織固有の必須項目は未確認です。以下の既存画像は旧版の参考で、今回のコードが動作した証拠ではありません。











旧版コードの参照先はMOTOKI-LLC/cg-method-codeです。今回の本文コードとの同期は確認していないため、関数を混ぜず本文の例を単独のプロジェクトで使ってください。





